<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Untrod</title><link href="http://blog.untrod.com/" rel="alternate"></link><link href="http://blog.untrod.com/feeds/all.atom.xml" rel="self"></link><id>http://blog.untrod.com/</id><updated>2024-04-14T00:00:00-07:00</updated><entry><title>LLM-Powered Django Admin Fields</title><link href="http://blog.untrod.com/2024/04/llm-chatgpt-powered-django-admin-fields.html" rel="alternate"></link><published>2024-04-14T00:00:00-07:00</published><updated>2024-04-14T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2024-04-14:/2024/04/llm-chatgpt-powered-django-admin-fields.html</id><summary type="html">&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: All relevant code can be found in the corresponding &lt;a href="https://github.com/chrisclark/django-llm-fields"&gt;Github repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I work at &lt;a href="https://www.grove.co"&gt;Grove Collaborative&lt;/a&gt; -- an ecommerce company that sells thousands of different products. We use Django and the Django admin as our product information system to manage, among other things, all of the copy on our Product …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: All relevant code can be found in the corresponding &lt;a href="https://github.com/chrisclark/django-llm-fields"&gt;Github repo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I work at &lt;a href="https://www.grove.co"&gt;Grove Collaborative&lt;/a&gt; -- an ecommerce company that sells thousands of different products. We use Django and the Django admin as our product information system to manage, among other things, all of the copy on our Product Detail Pages (PDPs). In order to more efficiently and consistently write copy for different PDP sections, we integrated ChatGPT directly into the Django Admin.&lt;/p&gt;
&lt;p&gt;While not &lt;em&gt;quite&lt;/em&gt; encapsulated enough to make it a standalone Django plugin, &lt;a href="https://github.com/chrisclark/django-llm-fields"&gt;the code&lt;/a&gt; is generic enough to quickly integrate ChatGPT with any Django text field, and provides a number of affordances to Django admin users to modify and test the underlying prompts.&lt;/p&gt;
&lt;h2&gt;What we did&lt;/h2&gt;
&lt;p&gt;In short, we now have little buttons next to Django Admin text fields that will generate copy according to a specific prompt and data payload. For example, we have a section on our PDPs titled "Why we believe in this product", backed by an attribute on the Product model called &lt;code&gt;why_we_believe&lt;/code&gt;. In the Django Admin, when viewing a Product model, there is a button that our merchandising team can use to populate the field. The text is editable and reviewable; ChatGPT provides initial copy, but humans are still very much in the loop.&lt;/p&gt;
&lt;p&gt;&lt;img alt="llm-django-field" src="http://blog.untrod.com/images/gpt-django-textarea.png"&gt;&lt;/p&gt;
&lt;p&gt;Each "LLM-enabled" text field is mapped to a specific prompt and bespoke data payload that provides the model-specific context to ChatGPT.&lt;/p&gt;
&lt;p&gt;We also have prompts and buttons for fields like "review summary" (summarizes the pros and cons of a product based on customer reviews), "hero ingredients", and more. The underlying prompts are editable in the admin so our merchants and tweak the output without engineering involvement:&lt;/p&gt;
&lt;p&gt;&lt;img alt="llm-django-prompts" src="http://blog.untrod.com/images/gpt-django-prompts.png"&gt;&lt;/p&gt;
&lt;p&gt;Finally, a playground area allows admin users to test the prompts out without leaving Django:&lt;/p&gt;
&lt;p&gt;&lt;img alt="llm-django-prompt-playground" src="http://blog.untrod.com/images/gpt-django-playground.png"&gt;&lt;/p&gt;
&lt;p&gt;We can quickly add GPT-powered copywriting to any text field, on any model, in the Django Admin. The relevant code is available in a &lt;a href="https://github.com/chrisclark/django-llm-fields"&gt;Github repo&lt;/a&gt;, with some instructions on how to get started. The balance of this post simply explicates the code and patterns.&lt;/p&gt;
&lt;h2&gt;How it works&lt;/h2&gt;
&lt;p&gt;Note that some of the code below is slightly simplified from what's in the repo in order to aid comprehension.&lt;/p&gt;
&lt;p&gt;First up, a simple model to store and manage the prompts that will power each text field:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GPTPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CoreModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;system_message_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;human_message_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CharField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blank&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegerField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;GPT_MODELS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;GPT3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;SystemMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;system_message_template&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;HumanMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;human_message_template&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GPT_MODELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__str__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After registering the model in the Django admin, the prompts are now editable.&lt;/p&gt;
&lt;p&gt;&lt;img alt="llm-django-prompt-admin" src="http://blog.untrod.com/images/gpt-django-prompt-admin.png"&gt;&lt;/p&gt;
&lt;p&gt;There is a bit of magic to the "key" value, which maps to a hard-coded value on a class that knows how to inject the proper contextual data for a given prompt. For example, the prompt in the screenshot above needs to know how to get &lt;code&gt;product_json&lt;/code&gt;. So we write a little class that can provide that data (one class for each prompt):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BelievePromptData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PROMPT_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;WHY_WE_BELIEVE_PROMPT&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;selling_points&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;manufacturer_description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;ingredients&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;product_json&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;})}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A single Django view will handle all of the client-side text-generation requests coming from the admin buttons:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generic_llm_prompt_view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promptDataCls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GPTPrompt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;promptDataCls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PROMPT_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;promptDataCls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;model_id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;HttpResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;()}),&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The correct PromptData class is injected when registering the URLs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;

&lt;span class="n"&gt;urlpatterns&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;re_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;^generate-believe-text/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generic_llm_prompt_view&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;promptDataCls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BelievePromptData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;generate-believe-text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note in the &lt;a href="https://github.com/chrisclark/django-llm-fields"&gt;repo&lt;/a&gt;, there are also URLs registered for the playground.&lt;/p&gt;
&lt;p&gt;To recap:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A request comes in to e.g. &lt;code&gt;generate-believe-text/?model_id=123&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The generic &lt;code&gt;llm_prompt_view&lt;/code&gt; passes the model_id into the PromptData object specified in the URL endpoint (in this case &lt;code&gt;BelievePromptData&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BelievePromptData&lt;/code&gt; gets the relevant model and turns it into the JSON blob that we want to inject into our prompt. The key of the PromptData class maps to the correct GPTPrompt object.&lt;/li&gt;
&lt;li&gt;The GPTPrompt gets loaded, merges in the JSON blob, and calls out to ChatGPT to generate the text.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This pattern makes it really easy to add more prompts; an admin user can create a new prompt (and test it out in the playground), and an engineer adds a corresponding PromptData class and new URL.&lt;/p&gt;
&lt;h2&gt;Hooking it up to the Django Admin UI&lt;/h2&gt;
&lt;p&gt;Lastly, we have to add the actual button next to the relevant field. Torturing the Django admin is never particularly fun, so we created a widget you can stick next to arbitrary model text fields, and map to the different URLs, corresponding to the various PromptData classes. The Widget is simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LLMTextAreaWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Textarea&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;template_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;widgets/llm_text_area_widget.html&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_req_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req_url&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;widget&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;llm_req_url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm_req_url&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;widget&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;btn_class&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-generate-btn&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The corresponding JavaScript is kind of a mess -- but functional. The HTML and JS for the widget can be found in the complete repo.&lt;/p&gt;
&lt;p&gt;Then simply add the widget to the admin form. E.g.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelForm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__all__&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;widgets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;why_we_believe&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LLMTextAreaWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/llm/generate-believe-text/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And voila! Our merchandisers can click the button to generate text and pre-populate the field. This fields remain a 'normal' text field; it's still overridable by the admin user. Merchants often use the buttons to get started, and then tweak the copy from there.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Robot Dad</title><link href="http://blog.untrod.com/2023/11/robot-dad.html" rel="alternate"></link><published>2023-11-26T00:00:00-08:00</published><updated>2023-11-26T00:00:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2023-11-26:/2023/11/robot-dad.html</id><summary type="html">&lt;p&gt;&lt;em&gt;See discussion of this post on &lt;a href="https://news.ycombinator.com/item?id=38433330"&gt;Hacker News&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Tired of Alexa's feeble "here's what I found on wikianswerspam.com" responses to my eight-year-old son's science questions, I whipped up Robot Dad during my Thanksgiving break. He now runs in the background of our family computer.&lt;/p&gt;
&lt;p&gt;Robot Dad sounds like real …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;See discussion of this post on &lt;a href="https://news.ycombinator.com/item?id=38433330"&gt;Hacker News&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Tired of Alexa's feeble "here's what I found on wikianswerspam.com" responses to my eight-year-old son's science questions, I whipped up Robot Dad during my Thanksgiving break. He now runs in the background of our family computer.&lt;/p&gt;
&lt;p&gt;Robot Dad sounds like real dad, thanks to voice cloning from &lt;a href="https://elevenlabs.io/speech-synthesis"&gt;Eleven Labs&lt;/a&gt; (very easy; I rambled about Formula 1 into a new MacOS Voice Memo for about sixty seconds, uploaded it, and voila), and answers appropriately for an eight-year-old, while deflecting prank requests -- though I suspect prompt injection will soon be second nature to this generation.&lt;/p&gt;
&lt;p&gt;Here's my son, Dash, interacting with Robot Dad.&lt;/p&gt;
&lt;p&gt;&lt;audio controls preload="none" style="width:480px;"&gt;
 &lt;source src="http://blog.untrod.com/media/robot-dad-demo.m4a" type="audio/mp4" /&gt;
&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;The delays are real, and the speech-to-text is only so-so, but it manages to be just good enough to clear the "provides value" bar. Robot Dad will also inject context from the previous question into the prompt. Dash could follow up with "Robot Dad, tell me more about it" and ChatGPT would know what to do.&lt;/p&gt;
&lt;p&gt;A few dozen lines of code glues together different AI services, for a remarkable result. The wakeword and speech-to-text happen locally, while the AI response (ChatGPT) and text-to-speech are via HTTP. It's trivial to move the LLM bit to a local Llama2 instance, but I haven't found a satisfactory text-to-speech model that can do voice cloning locally.&lt;/p&gt;
&lt;p&gt;I also made a quick speech visualization (turns out kids are not very engaged reading console log messages) that of course ended up providing more entertainment value than the &lt;em&gt;ENTIRE MODERN MIRACLE OF ARTIFICIAL INTELLIGENCE&lt;/em&gt;. Code for the visualization is in &lt;a href="https://gist.github.com/chrisclark/b9e7ba61654313a1e2d4a796ad5bb8a9"&gt;this gist&lt;/a&gt;.&lt;/p&gt;
&lt;video controls="controls" width="100%" name="speech visualization"&gt;
  &lt;source src="http://blog.untrod.com/media/robot-dad-speech-visualization.mov"&gt;
&lt;/video&gt;

&lt;p&gt;Code for Robot Dad is below. You will need API keys for &lt;a href="https://picovoice.ai/"&gt;Picovoice&lt;/a&gt; (and a wakeword), &lt;a href="https://elevenlabs.io/speech-synthesis"&gt;Eleven Labs&lt;/a&gt;, and OpenAI. You can pick a pre-existing Eleven Labs voice or clone your own.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pvporcupine&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;pvcheetah&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pvrecorder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PvRecorder&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;elevenlabs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;voices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;openai&lt;/span&gt;

&lt;span class="n"&gt;ENDPOINT_DURATION_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="c1"&gt;# &amp;#39;Quiet&amp;#39; seconds indicating the end of audio capture&lt;/span&gt;
&lt;span class="n"&gt;VOICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Dad&amp;#39;&lt;/span&gt; &lt;span class="c1"&gt;# Via Eleven Labs&lt;/span&gt;
&lt;span class="n"&gt;AUDIO_DEVICE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;MacBook Pro Microphone&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;AUDIO_DEVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PvRecorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_available_devices&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AUDIO_DEVICE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;OPENAI_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;gpt-3.5-turbo-1106&amp;#39;&lt;/span&gt;

&lt;span class="n"&gt;BASE_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;You are Robot Dad, and will be speaking with one of my children,&lt;/span&gt;
&lt;span class="s2"&gt;trying to be a helpful parent. You explain things at a level appropriate for&lt;/span&gt;
&lt;span class="s2"&gt;an eight-year-old.&lt;/span&gt;

&lt;span class="s2"&gt;You are encouraging and helpful, but won&amp;#39;t tolerate any inappropriate requests&lt;/span&gt;
&lt;span class="s2"&gt;or attempts at pranks or jokes. If you are asked or told anything&lt;/span&gt;
&lt;span class="s2"&gt;inappropriate, you gently say &amp;quot;nice try - but Robot Dad isn&amp;#39;t falling for that!&amp;quot;&lt;/span&gt;

&lt;span class="s2"&gt;If you don&amp;#39;t know how to reply, simply say &amp;quot;I&amp;#39;m just Robot Dad, not real dad -&lt;/span&gt;
&lt;span class="s2"&gt;so I&amp;#39;m afraid I can&amp;#39;t help you with that&amp;quot;.&lt;/span&gt;

&lt;span class="s2"&gt;You usually answer in no more than 4 sentences - kids do not have long attention&lt;/span&gt;
&lt;span class="s2"&gt;spans - but you can provide longer answers if it&amp;#39;s clearly needed.&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;PREV_CTX_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="s2"&gt;The last request and response you received is below. The next request may or may&lt;/span&gt;
&lt;span class="s2"&gt;not be a continuation of this conversation.&lt;/span&gt;

&lt;span class="s2"&gt;Previous request:&lt;/span&gt;
&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&lt;/span&gt;

&lt;span class="s2"&gt;Previous response:&lt;/span&gt;
&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;PREV_CTX_TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="c1"&gt;# seconds&lt;/span&gt;

&lt;span class="n"&gt;keyword_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;/wakewords/Robot-Dad.ppn&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;porcupine_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;PORCUPINE_API_KEY&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;OPENAI_API_KEY&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;porcupine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pvporcupine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;porcupine_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;keyword_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;keyword_paths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cheetah&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pvcheetah&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;access_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;porcupine_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;endpoint_duration_sec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ENDPOINT_DURATION_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;enable_automatic_punctuation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;recorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PvRecorder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;frame_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;porcupine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;device_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AUDIO_DEVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;break_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Got it! Robot Dad is thinking...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VOICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;alert_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;What&amp;#39;s up kiddo?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VOICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llm_req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;system&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Here is what the child has said: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatCompletion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;OPENAI_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;choices&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;message&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;content&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="c1"&gt;# Speech-to-text using Picovoice&amp;#39;s Cheetah&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;capture_input&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;partial_transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cheetah&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;partial_transcript&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;cheetah&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;play_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;audio_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
    &lt;span class="n"&gt;audio_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Listening...&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;prev_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
    &lt;span class="n"&gt;prev_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
    &lt;span class="n"&gt;last_wake_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;pcm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;porcupine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pcm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Detected Robot Dad&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;play_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alert_audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BASE_PROMPT&lt;/span&gt;
                &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_wake_time&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_wake_time&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;PREV_CTX_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;PREV_CTX_PROMPT&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prev_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;last_wake_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_time&lt;/span&gt;

                &lt;span class="n"&gt;transcript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capture_input&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Heard request: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;prev_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;

                &lt;span class="n"&gt;play_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;break_audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm_req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Answering: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;prev_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;

                &lt;span class="n"&gt;resp_audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VOICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp_audio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyboardInterrupt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Stopped.&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="Everything Else"></category></entry><entry><title>Copy Editing a Novel with ChatGPT</title><link href="http://blog.untrod.com/2023/06/copy-editing-a-novel-with-chatgpt.html" rel="alternate"></link><published>2023-06-28T00:00:00-07:00</published><updated>2023-06-28T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2023-06-28:/2023/06/copy-editing-a-novel-with-chatgpt.html</id><summary type="html">&lt;p&gt;My wife, a writer, recently and unexpectedly needed a final round of copy-editing on a manuscript she had written some time ago. There wasn't time to hire a professional, and even splitting it up, we couldn't get it done between the two of us fast enough.&lt;/p&gt;
&lt;p&gt;Luckily, I know Python …&lt;/p&gt;</summary><content type="html">&lt;p&gt;My wife, a writer, recently and unexpectedly needed a final round of copy-editing on a manuscript she had written some time ago. There wasn't time to hire a professional, and even splitting it up, we couldn't get it done between the two of us fast enough.&lt;/p&gt;
&lt;p&gt;Luckily, I know Python and have an OpenAI API key! Thus, I asked ChatGPT to copy-edit the book. I believed this was in ChatGPT's wheelhouse for a few reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Copy-editing is about true errors; spelling, grammar, etc. So "chunks" of text can be copy-edited separately. The entire novel doesn't need to be in the model's context window. Searching for plot holes or timeline inconsistencies in a novel-length text would be another matter entirely.&lt;/li&gt;
&lt;li&gt;ChatGPT has impeccable spelling and grammar. Therefore I assumed it would be good at catching errors in text.&lt;/li&gt;
&lt;li&gt;It's easy to provide good examples. "Few shot" prompting tends to improve performance.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Below we'll look at the approach I took, and the results. I've included illustrative, but incomplete, code. &lt;a href="https://gist.github.com/chrisclark/612ab8fa9c4c6dd5a85c1529162e0efd"&gt;Full code is available here&lt;/a&gt;. I used GPT4 as a coding assistant for much of this. And boy it's great. It's so nice to get working directory-traversal code (for instance) that does just what I need, without spending five minutes Googling and reading docs. GPT4-assisted Python scripting makes exercises like this much more enjoyable.&lt;/p&gt;
&lt;p&gt;To preview the conclusion: ChatGPT performed much less well than I thought, but still provided a usable copy editing pass. It is significantly better than nothing, but surprisingly noisy and erratic.&lt;/p&gt;
&lt;h2&gt;Chunking up the text&lt;/h2&gt;
&lt;p&gt;Using some handy ChatGPT-provided code, I chunked the novel into separate text files of (in this case) at most 1000 words. This is well under the maximum GPT4 context size of 8000 tokens (1000 words is perhaps 1250 tokens), but I found that smaller chunks lead to more consistency in finding true errors.&lt;/p&gt;
&lt;p&gt;Separate files made downstream experimentation easier (I could delete all but a few chunks; I could write resume code when the openai API failed, etc). The chunking is deterministic and fast.&lt;/p&gt;
&lt;h2&gt;Copy Editing with ChatGPT&lt;/h2&gt;
&lt;p&gt;Now we send the chunks to Openai and ChatGPT. The prompt below works...OK. I should have perhaps provided more rigorous few-shot examples. Nonetheless, I experimentally determined that the examples below did lend considerable consistency to the output. Without it, ChatGPT will come up with different ways of providing the feedback for each chunk, which makes the results hard for a human to process, and a machine to parse (more on that later). If I did this again, I'd spend another iteration or two on the prompt. I do believe there is quality juice to squeeze there.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You are a copy editor looking for issues in a novel before it is submitted to publishers. You are looking for obvious grammar and spelling issues and any information that is obviously incorrect. Do not make suggestions related to style, or edits for clarity. Just focus on copy errors.&lt;/p&gt;
&lt;p&gt;Please format your responses as a series of bullet points. Start with a quote of a few words from the novel that you are copy-editing (so it's easy to find in the novel), then follow with your comments/corrections.&lt;/p&gt;
&lt;p&gt;Here are some examples of good copy edits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"an two hundred year" -&amp;gt; "a two hundred year"&lt;/li&gt;
&lt;li&gt;"on to the veranda" -&amp;gt; "onto the veranda"&lt;/li&gt;
&lt;li&gt;"full memory of the night" -&amp;gt; "full memories of the night"&lt;/li&gt;
&lt;li&gt;"Her and Luis's dog" -&amp;gt; "Her and Luis' dog"&lt;/li&gt;
&lt;li&gt;"Felicia stiffened almost indecipherably." -&amp;gt; "Felicia stiffened almost imperceptibly."&lt;/li&gt;
&lt;li&gt;"The is the family kitchen." -&amp;gt; "This is the family kitchen."&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do not suggest substitutions of one type of punctuation mark for another. For example, do not suggest replacing ` with ', or “ with ".&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Looping through each of the 104 chunks with GPT4 took about 35 minutes and cost just over $8 in API fees. In retrospect, for this task, I suspect GPT3.5 could give equivalent results more quickly, at less cost.&lt;/p&gt;
&lt;h2&gt;Initial Result&lt;/h2&gt;
&lt;p&gt;So...does it work? Not as well as I'd hoped. Perhaps 1/3 of the suggestions are legitimate suggestions/corrections. The others fall into one of three categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Non-corrections. The original text and the new text are identical, or simply changed a quote or apostrophe type.&lt;ul&gt;
&lt;li&gt;"I didn’t tell the police." -&amp;gt; "I didn't tell the police."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Oddly specific stylistic suggestions (in spite of the prompt asking to avoid these):&lt;ul&gt;
&lt;li&gt;"She yelped. Savvy tried to apologize" -&amp;gt; "She yelped, and Savvy tried to apologize"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Total hallucinations. This text, for instance, simply doesn't appear in the corresponding chunk:&lt;ul&gt;
&lt;li&gt;"highway ears as they passed" -&amp;gt; "highway ears as it passed"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Trying to read through an output riddled with false-positives is mentally exhausting. Which of these are legitimate suggestions and which contain no signal?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"bulletproof glass partition. “You" -&amp;gt; "bulletproof glass partition, "You"&lt;/li&gt;
&lt;li&gt;"Detective Moorehead, please,” Savvy" -&amp;gt; "Detective Moorehead, please," Savvy"&lt;/li&gt;
&lt;li&gt;"loudly banging each letter" -&amp;gt; "loudly, banging each letter"&lt;/li&gt;
&lt;li&gt;"Finished. Straightened a stack" -&amp;gt; "Finished, straightened a stack"&lt;/li&gt;
&lt;li&gt;"What is this in" -&amp;gt; "What is this in,"&lt;/li&gt;
&lt;li&gt;"with Detective Moorehead,” she" -&amp;gt; "with Detective Moorehead," she"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's like a vision test. Yikes.&lt;/p&gt;
&lt;p&gt;After carefully reading chatGPT's output, I wrote a function to detect hallucinations (19 of them for those keeping score at home), non-corrections, and persnickety suggestions that simply involved swapping a backtick for an apostrophe (e.g. encoding nuances).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_real_correction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;left_side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_str&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; -&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;’&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;“&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;”&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;right_side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;input_str&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; -&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;’&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;“&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;”&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left_side&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;right_side&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;left_side&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;left_side&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# full_novel_text check is for hallucinations&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;left_side&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;right_side&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;remove extra space&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;left_side&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;full_novel_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;left_side&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;right_side&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Final Result&lt;/h2&gt;
&lt;p&gt;For a novel of about 90,000 words, GPT4 identified 1120 "issues", which the script above reduced to 811. I suspect feeding the corrections back into ChatGPT with a prompt to identify only real corrections could have worked -- but good old-fashioned string manipulation with Python is also plenty good. I'm not yet reaching for ChatGPT to solve all of my problems, but perhaps I am a luddite.&lt;/p&gt;
&lt;p&gt;Of the remaining corrections, about 25% are truly legitimate corrections, 50% are somewhat random wording and stylistic changes, and the remaining 25% are legitimate grammar issues, but are contained within spoken dialogue or are otherwise used purposefully. If this was a copy-edit for a magazine article, this last 25% would be useful, as the author would want the writing to conform strictly to a standard (e.g. Chicago Manual of Style) -- but is of limited utility in the context of a novel.&lt;/p&gt;
&lt;p&gt;Here are some representative corrections.&lt;/p&gt;
&lt;p&gt;Legitimate Copy Errors&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Felicia stiffened almost indecipherably. -&amp;gt; Felicia stiffened almost imperceptibly.&lt;/li&gt;
&lt;li&gt;The is the family kitchen. -&amp;gt; This is the family kitchen.&lt;/li&gt;
&lt;li&gt;"No, doesn’t mean that" -&amp;gt; "No, it doesn't mean that"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Random Rephrasings&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"He stood on the opposite side" -&amp;gt; "He stood on the other side"&lt;/li&gt;
&lt;li&gt;"He put his glass down" -&amp;gt; "He set his glass down"&lt;/li&gt;
&lt;li&gt;"the overhead lights flashed" -&amp;gt; "the overhead lights flickered"&lt;/li&gt;
&lt;li&gt;"The corners of his mouth" -&amp;gt; "The corners of his lips"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Debatable Copy Corrections&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"His laugh was contagious and his spirit big, I'm told.” -&amp;gt; "His laugh was contagious, and his spirit was big, I'm told.”&lt;/li&gt;
&lt;li&gt;"Not exactly the apology Savvy was expecting" -&amp;gt; "Not exactly the apology Savvy expected"&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Yeah - I mean - not bad? Kind of OK for $8? (Ignoring the ~2 hours of time it took me to wrangle the code)&lt;/p&gt;
&lt;p&gt;I can't help but feel a little disappointed, as this felt like a slam-dunk for ChatGPT. As the &lt;a href="https://cdixon.org/2009/08/20/machine-learning-is-really-good-at-partially-solving-just-about-any-problem"&gt;old saying goes&lt;/a&gt;, AI is great at solving 80% of any problem.&lt;/p&gt;
&lt;p&gt;With that said, I do think ChatGPT (or even a much simpler model) could be turned into an excellent copywriter through fine tuning. The tech is surely capable of doing much better. Given some more time, I would have loved to try that approach (especially because training data is easy to generate for this use case). Perhaps in a future blog post...&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Short Stories for Engineers</title><link href="http://blog.untrod.com/2022/06/scifi-short-stories-for-engineers.html" rel="alternate"></link><published>2022-06-09T00:00:00-07:00</published><updated>2022-06-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-06-09:/2022/06/scifi-short-stories-for-engineers.html</id><summary type="html">&lt;p&gt;I've recently been reminded of a few favorite short stories, and discovered some new ones, that swirl around themes of existence, engineering. order, systems, computing, and information.&lt;/p&gt;
&lt;p&gt;None of these are deep cuts; they are well known among sci-fi readers and short story lovers. But I thought I'd compile the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've recently been reminded of a few favorite short stories, and discovered some new ones, that swirl around themes of existence, engineering. order, systems, computing, and information.&lt;/p&gt;
&lt;p&gt;None of these are deep cuts; they are well known among sci-fi readers and short story lovers. But I thought I'd compile the ones I love nonetheless.&lt;/p&gt;
&lt;h3&gt;Isaac Asimov (1956) - The Last Question (and Answer)&lt;/h3&gt;
&lt;p&gt;The Last Question is Asmiov's best story, and everyone knows it (Asimov included). A later story, The Last Answer, is also excellent but related only indirectly, through an existential theme. It's worth noting that &lt;a href="https://www.roma1.infn.it/~anzel/answer.html"&gt;Answer&lt;/a&gt; by Fredric Brown is likely an (uncited) inspiration (it was written a few years prior). Lastly, a Douglas Adams fan will surely recognize the famous Last Question parody from Hitchiker's Guide.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.physics.princeton.edu/ph115/LQ.pdf"&gt;The Last Question&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://highexistence.com/the-last-answer-short-story/"&gt;The Last Answer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Harlan Ellison (1965) - "Repent, Harlequin!", Said the Ticktockman&lt;/h3&gt;
&lt;p&gt;Nearly a period piece; bizarre and subverts expectations in terms of writing and style, while having a plot that appears cliche, except that the cliche originated with this story. In other words, purely original, but the ideas have entered collective consciousness such that they seems well worn.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.d.umn.edu/~tbacig/cst1010/chs/ellison.html"&gt;"Repent, Harlequin!", Said the Ticktockman&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ted Chiang (2014) - Exhalation&lt;/h3&gt;
&lt;p&gt;Boy oh boy Ted Chiang is exciting. Story of Your Life is another short of his that provided the plot of the excellent movie Arrival. Apart from that, this is likely his best-known story, and my favorite.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.lightspeedmagazine.com/fiction/exhalation/"&gt;Exhalation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Roald Dahl (1954) - The Great Automatic Grammatizator&lt;/h3&gt;
&lt;p&gt;Particularly interesting in the context of GPT-3 et al.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://d-a-v-e.org/wp/wp-content/uploads/2020/09/The_Great_Automatic_Grammatizator.pdf"&gt;The Great Automatic Grammatizator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Andy Weir (2009) - The Egg&lt;/h3&gt;
&lt;p&gt;Confession: This is not actually one of my favorites. It's nominally as thought-provoking as any other story here, but it just doesn't stick in my mind the same way as some others. Nonethless, it's enjoyable and so thematically close to some of the other stories that I had to include it in this list.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.galactanet.com/oneoff/theegg_mod.html"&gt;The Egg&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Kurt Vonnegut (1961) - Harrison Bergeron&lt;/h3&gt;
&lt;p&gt;Perhaps you've heard of the Handicapper General? It's hard to believe this was written in 1961. In fact, it's a good reminder that we are prisoners of our own experience and societal issues that may seem novel are not new at all.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://www.tnellen.com/cybereng/harrison.html"&gt;Harrison Bergeron&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Everything Else"></category></entry><entry><title>Guidepost - Business Value</title><link href="http://blog.untrod.com/2022/04/guidepost-business-value.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-business-value.html</id><summary type="html">&lt;p&gt;We write code to deliver business value. We should therefore select solutions where most of the investment is going into the differentiated bits that drive business value.&lt;/p&gt;
&lt;p&gt;This means we want to write more code that is only relevant to Grove (because it uniquely solves our business problems) and less …&lt;/p&gt;</summary><content type="html">&lt;p&gt;We write code to deliver business value. We should therefore select solutions where most of the investment is going into the differentiated bits that drive business value.&lt;/p&gt;
&lt;p&gt;This means we want to write more code that is only relevant to Grove (because it uniquely solves our business problems) and less code that is related to general scaling or infrastructure challenges. If we can buy solutions to the other problems (or just side-step them), let’s do that, and take advantage of other customers of that 3rd party solution subsidizing the cost for us.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - Getting to Production</title><link href="http://blog.untrod.com/2022/04/guidepost-getting-to-production.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-getting-to-production.html</id><summary type="html">&lt;p&gt;We’ve always prioritized getting to production frequently. Lots of goodness flows from shipping early and often. The problems associated with shipping too frequently are so much easier to fix than the problems of shipping too infrequently.&lt;/p&gt;
&lt;p&gt;When releases are infrequent, there is a natural tendency to “shrink to fit …&lt;/p&gt;</summary><content type="html">&lt;p&gt;We’ve always prioritized getting to production frequently. Lots of goodness flows from shipping early and often. The problems associated with shipping too frequently are so much easier to fix than the problems of shipping too infrequently.&lt;/p&gt;
&lt;p&gt;When releases are infrequent, there is a natural tendency to “shrink to fit”; to cut a corner or create pressure to hit a release date, lest your code has to wait until the next release. Even a release calendar like e.g. “Release once per day, except Friday” is one bad Thursday release away from 5 days between releases — enough to create artificial pressure that works directly against all efforts to ship quality software.&lt;/p&gt;
&lt;p&gt;Getting to production frequently means that releases are no big deal, and reverts are no big deal.&lt;/p&gt;
&lt;p&gt;That’s not to say “more releases are always better” — we are already at a point where it’s impractical to manage when code is being released, and it’s challenging to determine the root cause of new exceptions because releases are so frequent. But the appropriate rate of releases is likely higher than intuition would suggest.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - How Long Should Software Last?</title><link href="http://blog.untrod.com/2022/04/guidepost-how-long-should-software-last.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-how-long-should-software-last.html</id><summary type="html">&lt;p&gt;At each stage of the business, there is a correspondingly appropriate length of time you should plan for your code to last - the horizon of the software’s utility. In the very early stages of a start-up, code may only need to last a few days. “We’ll try it …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At each stage of the business, there is a correspondingly appropriate length of time you should plan for your code to last - the horizon of the software’s utility. In the very early stages of a start-up, code may only need to last a few days. “We’ll try it, and if it works, we can rewrite it next week!”.&lt;/p&gt;
&lt;p&gt;And for a series B company, it doesn’t make sense to write software that will take the company public, but you also can’t have code breaking at 2x the scale and rewriting every few months.&lt;/p&gt;
&lt;p&gt;In a late stage company (where Grove is today), your code should last for about 3 years. If it has to be rewritten within 12 months, then it is not robust enough. If it breaks in 2-3 years due to growth and/or changes in the business, then so be it. You did your job. But of course, this is a floor on utility – not an ask for planned obsolescence. Obviously code that costs the same to create, and lasts longer, is always preferable.&lt;/p&gt;
&lt;p&gt;You don’t need to solve for scale problems that aren’t within the current horizon.&lt;/p&gt;
&lt;p&gt;Note that experiments are entirely different. We may write a feature experimentally, which, if it works, will need to be re-written to meet our production/lifetime horizon criteria. Earlier in the company’s history we (like many) had a poor track record of actually re-writing these things and would let hacky experiment code live for a long time. We don’t do that anymore. It’s frustrating for the business, to see the “B” variation of a test do really well, and then have to wait multiple sprints to see it scaled out to 100% of customers -- but that’s the price of operational code that has to last for multiple years.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - Scaling Principles</title><link href="http://blog.untrod.com/2022/04/guidepost-scaling-principles.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-scaling-principles.html</id><summary type="html">&lt;p&gt;Distributed systems are hard. We should exhaust many options before the solution is “do something distributed”. This is also why we are fine creating services, but are not advocates of “micro services”.&lt;/p&gt;
&lt;p&gt;Similarly, in most cases we should exhaust vertical scaling before moving to horizontal. It’s almost always less …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Distributed systems are hard. We should exhaust many options before the solution is “do something distributed”. This is also why we are fine creating services, but are not advocates of “micro services”.&lt;/p&gt;
&lt;p&gt;Similarly, in most cases we should exhaust vertical scaling before moving to horizontal. It’s almost always less expensive to buy a larger box than it is to staff a complex distributed systems project. In a business like Grove’s, we should make prudent financial decisions with our tech choices, but ultimately the economic drivers of the business are elsewhere — not in technology OpEx. Note that other companies, mostly SaaS companies that are, on some level, reselling infrastructure; Mixpanel, Segment, etc, do have tech OpEx as a key part of cost structure. It’s just not the case here.&lt;/p&gt;
&lt;p&gt;We believe that better code can address a lot of scaling challenges; it’s just a website selling soap, after all. While we are “at scale” from a revenue standpoint, we are relatively low-volume in the scheme of web applications, simply because of the size and access patterns of our customers. This is not to trivialize the complexity of scaling, but we believe that, at this stage, the answer is more likely to be “create a better data model with smarter caching” rather than “shard the database”.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - Services &amp; Monoliths</title><link href="http://blog.untrod.com/2022/04/guidepost-services-and-monoliths.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-services-and-monoliths.html</id><summary type="html">&lt;p&gt;Monoliths are not inherently problematic. Perhaps they are even good! Close coupling is the evil to avoid. With good design, monoliths can avoid tight coupling. Services can also be a valuable tool for achieving this. But the purpose of decreasing coupling is to decrease overall complexity. Therefore a service that …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Monoliths are not inherently problematic. Perhaps they are even good! Close coupling is the evil to avoid. With good design, monoliths can avoid tight coupling. Services can also be a valuable tool for achieving this. But the purpose of decreasing coupling is to decrease overall complexity. Therefore a service that decreases coupling, but increases overall complexity (e.g. introduces fiendish distributed systems problems), is to be avoided.&lt;/p&gt;
&lt;p&gt;When we say “service” we really mean “a piece of software with a well-defined boundary”, which doesn’t necessarily mean “web service that ships and operates independently of the monolith”.&lt;/p&gt;
&lt;p&gt;A more formal definition might be “functionality that is accessed using a prescribed interface and conforms to constraints and policies specified by a service description”. Note this definition does not say “and it needs a separate CI/CD pipeline”.&lt;/p&gt;
&lt;p&gt;Services are also a management tool.&lt;/p&gt;
&lt;p&gt;Conway’s Law is, broadly, that you ship your org chart; if you are building a compiler, and you have three teams working on it, you’ll get a three-pass compiler. Services are useful through this lens. They create boundaries between pieces of technology so that small teams can operate independently and have ownership of their results.&lt;/p&gt;
&lt;p&gt;Create service boundaries when it’s useful to create ownership over a problem domain and when the service decreases net complexity.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - Technology &amp; Language Selection</title><link href="http://blog.untrod.com/2022/04/guidepost-technology-language-selection.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-technology-language-selection.html</id><summary type="html">&lt;p&gt;There is value in consistency, and fungibility (e.g. the extent to which a developer can work on multiple projects). There’s value in better bus factors.&lt;/p&gt;
&lt;p&gt;Therefore, there is a high bar for selecting a new technology that the team doesn’t know.&lt;/p&gt;
&lt;p&gt;At some level, programming languages are …&lt;/p&gt;</summary><content type="html">&lt;p&gt;There is value in consistency, and fungibility (e.g. the extent to which a developer can work on multiple projects). There’s value in better bus factors.&lt;/p&gt;
&lt;p&gt;Therefore, there is a high bar for selecting a new technology that the team doesn’t know.&lt;/p&gt;
&lt;p&gt;At some level, programming languages are just syntax and don’t matter that much -- but the lift in a new language/stack’s ecosystem can be considerable, and beginning with something new makes beginners’ mistakes more likely.&lt;/p&gt;
&lt;p&gt;We default to the things we already use, unless there is a strong reason that it won’t work.&lt;/p&gt;
&lt;p&gt;Note this is different from iterative improvement; introducing a new library or a new pattern or a new practice is different than introducing an entirely new technology or language. We should absolutely be pushing to modernize our Python, JavaScript, SQL, etc. development practices; this is not a case for stagnation, but a case for uniformity-by-default.&lt;/p&gt;
&lt;p&gt;Technologies don’t get selected simply because they are interesting.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Guidepost - Testing</title><link href="http://blog.untrod.com/2022/04/guidepost-testing.html" rel="alternate"></link><published>2022-04-09T00:00:00-07:00</published><updated>2022-04-09T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2022-04-09:/2022/04/guidepost-testing.html</id><summary type="html">&lt;p&gt;The primary value of great tests is that it allows developers (especially those who didn’t write the original code!) to refactor. With tests at your back, you can refactor with confidence. Without the ability to refactor, our velocity will slowly decrease to zero.&lt;/p&gt;
&lt;p&gt;We love automated testing. If you …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The primary value of great tests is that it allows developers (especially those who didn’t write the original code!) to refactor. With tests at your back, you can refactor with confidence. Without the ability to refactor, our velocity will slowly decrease to zero.&lt;/p&gt;
&lt;p&gt;We love automated testing. If you have ever found yourself deploying code to production based solely on your automated tests, and realizing afterwards - “Oh sh**! I never actually tested this manually!”, then you are doing it right. It is impossible for developers to have confidence their code works without tests.&lt;/p&gt;
&lt;p&gt;If you think of tests as “homework”, or something to be added at the end, once it’s working - keep working at automated testing! Tests are your buddy along the development journey, ensuring at each step along the way that your software works the way you expect. If that feeling is not deep in your bones, keep working at it! Train yourself to find the joy and confidence in writing tests as you go.&lt;/p&gt;
&lt;p&gt;It’s a very normal state of affairs that your change will have more test code than application code.&lt;/p&gt;
&lt;p&gt;As you move from unit tests to integration tests, you trade off diagnostic utility for coverage; unit tests cover little, but when they fail, it’s immediately clear what went wrong. Integration tests can cover a huge variety of regressions, but when they fail it’s harder to understand why. Use both.&lt;/p&gt;
&lt;p&gt;The broadest tests of all are browser/device automation tests. They are there to ensure that nothing terrible is about to happen in production -- not to ensure correctness of your code.&lt;/p&gt;
&lt;p&gt;We aren’t believers in test-driven-development; it’s a great way to never get anything done.&lt;/p&gt;</content><category term="Guideposts"></category></entry><entry><title>Adventures in Candy Land</title><link href="http://blog.untrod.com/2021/07/adventures-in-candy-land.html" rel="alternate"></link><published>2021-07-23T00:00:00-07:00</published><updated>2021-07-23T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2021-07-23:/2021/07/adventures-in-candy-land.html</id><summary type="html">&lt;p&gt;I play Candy Land occasionally with my two boys (the six year old gets
it much more than the three year old). It has the strange property of
involving both zero skill and zero agency. There are absolutely no
decisions for the player to make at any point; the outcome …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I play Candy Land occasionally with my two boys (the six year old gets
it much more than the three year old). It has the strange property of
involving both zero skill and zero agency. There are absolutely no
decisions for the player to make at any point; the outcome of the game
is determined as soon as the cards are shuffled.&lt;/p&gt;
&lt;p&gt;This makes it reduce to a game that is about as fun as flipping a
coin, but significantly &lt;em&gt;more&lt;/em&gt; fun to hack together in Python and
explore!&lt;/p&gt;
&lt;p&gt;We'll do a rules refresher in a sec, but first, some simple classes
representing the board, a player, and a "move" object, which doesn't
affect gameplay, but will keep track of each turn so we can analyze
games later.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shortcuts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;specials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;licorice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortcuts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shortcuts&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specials&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licorice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;licorice&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;//&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;specials&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_position&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_position&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we can create a variety of different Candy Land boards. Here's a
board with full fidelity to the original game (which you can see
&lt;a href="http://blog.untrod.com/images/candyland1.jpg"&gt;here&lt;/a&gt;). For the morbidly curious, I
got the right indices for the licorice, specials, etc by laying out
the board in a
&lt;a href="http://blog.untrod.com/files/candyland/candy-land-board.csv"&gt;spreadsheet&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;134&lt;/span&gt;
&lt;span class="n"&gt;COLORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;RPYBOG&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SPECIALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Plumpy&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Mr. Mint&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Jolly&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Gramma Nut&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Princess Lolly&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;Queen Frostine&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;103&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;LICORICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SHORTCUTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHORTCUTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPECIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LICORICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spc_fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licorice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Licorice: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortcuts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Shortcut to &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s1"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortcuts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;spc_fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's our board with some formatting to signify licorice and shortcuts:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Shortcut to 58: O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Plumpy&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Mr. Mint&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Shortcut to 46: P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Jolly&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Licorice: Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Gramma Nut&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Licorice: B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Princess Lolly&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Queen Frostine&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Licorice: R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;O&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;R&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;P&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Good stuff.&lt;/p&gt;
&lt;p&gt;Ok, onto the game! For those of you who have &lt;em&gt;not&lt;/em&gt; played Candy Land
recently, you can read the official rules
&lt;a href="http://blog.untrod.com/files/candyland/cl_rules.pdf"&gt;here&lt;/a&gt;, but the most salient
bits are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Players take turns drawing cards and then...do what the card
  says. Again...there is zero decision making on the part of the
  player.&lt;/li&gt;
&lt;li&gt;In the original game (which is mirrored in the settings above),
  there are 66 cards; 6 "special character" cards, and 60 color
  cards. There are six colors, with eight "single" and two "double"
  cards in each color. The game code (which we'll look at next)
  generates the cards.&lt;/li&gt;
&lt;li&gt;When a player draws a color card, move forward to the next color of
  that space. For double color cards, move to the next-next color of
  that space.&lt;/li&gt;
&lt;li&gt;If the card is a "special character" card, go directly to that space
  (even if it is behind you).&lt;/li&gt;
&lt;li&gt;There are three "licorice" spaces. If you land on one of these, you
  are stuck until you draw that color again. Note that in more recent
  versions the player simply loses a turn on these spaces. We are
  keeping with the old-school "stuck" rules.&lt;/li&gt;
&lt;li&gt;There are two "shortcuts". If you land exactly on one of the
  shortcut spaces, immediately follow the shortcut.&lt;/li&gt;
&lt;li&gt;You win by getting to the "end" of the board (e.g. drawing a color
  that does not appear in front of you).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that in mind, here is the game code:&lt;/p&gt;
&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_cards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specials&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;\
               &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_cards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_gets_unstuck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specials&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt;\
               &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licorice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_gets_unstuck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;specials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;

        &lt;span class="c1"&gt;# Loop to handle &amp;#39;double&amp;#39; color cards&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spaces&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licorice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
                &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shortcuts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_take_turn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# arguably &amp;quot;too clever by half&amp;quot; way of determining turns&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_take_turn&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;moves&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;moves&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;p&gt;With default settings, the 66 cards of the deck are created in the
&lt;code&gt;create_cards&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;The core game logic is in &lt;code&gt;generate_move&lt;/code&gt;: given a card and a player,
we calculate the new position of the player. This is somewhat
interesting and it's the algorithm that must be running inside my
six-year-old's head, and the algorithm that my three-year-old can't
quite get a handle on.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;play&lt;/code&gt; game loop simply takes turns until the win condition is met
(which happens when the drawn card's position doesn't is not present
in the remainder of the board - see line 40).&lt;/p&gt;
&lt;p&gt;Now that we have our game objects, we can play (using the board we
created previously):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Chris&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Maggie&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;moves&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can print out the moves and see what happened. Weirdly, formating
the move well was perhaps the hardest part of this entire exercise.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fmt_move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;On turn &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s2"&gt; draws &lt;/span&gt;&lt;span class="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&gt; and WINS!&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;licorice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;On turn &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s2"&gt; draws &lt;/span&gt;&lt;span class="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&gt; and is stuck on &lt;/span&gt;&lt;span class="si"&gt;{3}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;On turn &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s2"&gt; draws &lt;/span&gt;&lt;span class="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&gt; and moves to &lt;/span&gt;&lt;span class="si"&gt;{3}&lt;/span&gt;&lt;span class="s2"&gt;.&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_position&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;moves&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt_move&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;board&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's the output, eliding over a number of moves for brevity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;On turn 1, Chris draws Plumpy and moves to 9.
On turn 2, Maggie draws O and moves to 59.
...
On turn 27, Chris draws P and is stuck on 48.
On turn 28, Maggie draws GG and is stuck on 121.
On turn 29, Chris draws RR and is stuck on 48.
On turn 30, Maggie draws R and moves to 127.
On turn 31, Chris draws R and is stuck on 48.
On turn 32, Maggie draws Princess Lolly and moves to 95.
...
On turn 49, Chris draws RR and moves to 89.
On turn 50, Maggie draws R and moves to 127.
On turn 51, Chris draws Y and moves to 91.
On turn 52, Maggie draws GG and WINS!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Grr, I lost. Oh well - I'm sure there will be more games...in
fact...we can build a harness to play &lt;em&gt;many&lt;/em&gt; games, and see the effect
of the various board components:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;play_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Game&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;
    &lt;span class="n"&gt;lengths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Turn stats for &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt; &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39; games:&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Mean:    &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Median:  &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Min/max: &lt;/span&gt;&lt;span class="si"&gt;{0}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{1}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lengths&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;PLAYERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Chris&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;NUM_GAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;
&lt;span class="n"&gt;LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;134&lt;/span&gt;

&lt;span class="n"&gt;STANDARD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;players&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PLAYERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;board&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHORTCUTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPECIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LICORICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="n"&gt;NO_LICORICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;players&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PLAYERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;board&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHORTCUTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPECIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="n"&gt;NO_SPECIALS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;players&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PLAYERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;board&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHORTCUTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;LICORICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="n"&gt;NO_SHORTCUTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;players&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PLAYERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;board&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Board&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="n"&gt;COLORS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SPECIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LICORICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Standard&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;play_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STANDARD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NUM_GAMES&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;No Licorice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;play_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NO_LICORICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NUM_GAMES&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;No Specials&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;play_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NO_SPECIALS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NUM_GAMES&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;No Shortcuts&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;play_games&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NO_SHORTCUTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NUM_GAMES&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that these games have only one player (me!) since they are for
analytical purposes (vs. HARD-CORE COMPETITIVE purposes). The results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Turn stats for 10000 &amp;#39;Standard&amp;#39; games:
Mean:    38.6897
Median:  33.0
Min/max: 4/242

Turn stats for 10000 &amp;#39;No Licorice&amp;#39; games:
Mean:    34.5284
Median:  30.0
Min/max: 4/178

Turn stats for 10000 &amp;#39;No Specials&amp;#39; games:
Mean:    27.6359
Median:  28.0
Min/max: 10/74

Turn stats for 10000 &amp;#39;No Shortcuts&amp;#39; games:
Mean:    40.6276
Median:  35.0
Min/max: 5/210
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You now know a deep secret of the universe: that the 'special' cards
in Candy Land on average send players backwards more often than
forwards. My GOD! I always thought they were there to help! Doing some
simple math, we determine:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Impact of licorice: 4.16 turns
Impact of specials: 11.05 turns
Impact of shortcuts: -1.94 turns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Shortcuts reduce games by an average of 1.9 turns, while licorice and
specials add to the average lengths of games. Cool!&lt;/p&gt;
&lt;p&gt;I, for one, thoroughly enjoyed acquiring this utterly worthless
knowledge. There was never a chance of discovering a "better" way to
play Candy Land as the player has zero agency. But I suppose that's
the joy of Candy Land for small children; a perfectly even chance of
winning against your far more sophisticated, Python-programming
parents.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Explorations in The Card Game SET®</title><link href="http://blog.untrod.com/2021/06/set-solver-in-python.html" rel="alternate"></link><published>2021-06-28T00:00:00-07:00</published><updated>2021-06-28T00:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2021-06-28:/2021/06/set-solver-in-python.html</id><summary type="html">&lt;p&gt;&lt;a href="https://www.playmonster.com/brands/set/"&gt;SET&lt;/a&gt; is a classic pattern-matching card game that's been around since the 1970s, but really took off in the 90s. It hits all the right marks for a great game casual game; easy to learn, works for any number of players, you can play for 5 minutes or for hours …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://www.playmonster.com/brands/set/"&gt;SET&lt;/a&gt; is a classic pattern-matching card game that's been around since the 1970s, but really took off in the 90s. It hits all the right marks for a great game casual game; easy to learn, works for any number of players, you can play for 5 minutes or for hours, little equipment required, and it's always competitive.&lt;/p&gt;
&lt;p&gt;Here's how it works: using a special set of 81 cards, deal 12 at random, face up. Each card is has a unique combination of 4 attributes: Color, shape, fill, and number, each of which has three different possible values. So after dealing you get something like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="dealing-set" src="http://blog.untrod.com/images/set.png"&gt;&lt;/p&gt;
&lt;p&gt;Players shout "set!" when they identify a "valid set". A valid set consists of exactly three cards, for which each of the four attributes are either &lt;em&gt;all the same&lt;/em&gt; or &lt;em&gt;all different&lt;/em&gt;. So here's a valid set from the deal above:&lt;/p&gt;
&lt;p&gt;&lt;img alt="dealing-set" src="http://blog.untrod.com/images/valid-set.png"&gt;&lt;/p&gt;
&lt;p&gt;You can see that, for each of the four attributes, all the cards either completely match or are completely different.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Card 1&lt;/th&gt;
&lt;th&gt;Card 2&lt;/th&gt;
&lt;th&gt;Card 3&lt;/th&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Number&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Different&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fill&lt;/td&gt;
&lt;td&gt;Lined&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Solid&lt;/td&gt;
&lt;td&gt;Different&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Color&lt;/td&gt;
&lt;td&gt;Green&lt;/td&gt;
&lt;td&gt;Red&lt;/td&gt;
&lt;td&gt;Purple&lt;/td&gt;
&lt;td&gt;Different&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;Squiggle&lt;/td&gt;
&lt;td&gt;Squiggle&lt;/td&gt;
&lt;td&gt;Squiggle&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you were to change any attribute on any of the cards in the set, you'd have some attribute for which 2 cards matched, and the third different, thus making it an invalid set. When someone finds a set, you remove it, and deal three new cards in their place. You go through the entire deck until there are &amp;lt;=12 cards left, with no more sets. Whoever claimed the most sets wins.&lt;/p&gt;
&lt;p&gt;The rules are very simple, but the fun comes from staring at the cards as they are dealt, frantically trying to find sets before you opponent. If my brain had a cooling fan, it would immediately turn on as my cognitive load instantly maxes out.&lt;/p&gt;
&lt;p&gt;To understand a little better what I was looking for (and to fiddle around with Python, which is always fun), I sought to determine how many sets are likely to be in the first 12 dealt cards. Here's the code to generate a deck of SET cards, and detect whether a set (or multiple sets) are present. There are a few "unnecessary" methods in the below because (spoilers!) we are going to simulate a &lt;em&gt;lot&lt;/em&gt; of Set games later and this turned out to make quite a difference.&lt;/p&gt;
&lt;div class="callout"&gt;
  &lt;h3&gt;&gt; Run the code&lt;/h3&gt;
  &lt;p&gt;
  You can &lt;a href="https://colab.research.google.com/drive/1jTIiu-Ewagyi_KZgL7FXOCfsh-JqE0MH?usp=sharing"&gt;run and edit&lt;/a&gt; all of the code in this blog post right from your browser.
  &lt;/p&gt;
&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;itertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;combinations&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;number&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;fill&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;color&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;shape&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# 1, 2, 3, open, shaded, filled, purple, green, red, diamond, squiggle, oval&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{}{}{}{}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;123&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;osf&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;pgr&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;dso&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_valid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elems&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Members of elems are all the same, or all different&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elems&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="n"&gt;_valid&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt; &lt;span class="c1"&gt;# 4 is the number of distinct attributes&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_is_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# The same as above, but bails as soon as it finds a set. Again, performance thing.&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;combinations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_is_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sets_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Perf optimization: 21+ cards guarantee a set&lt;/span&gt;
    &lt;span class="c1"&gt;# via https://mathscinet.ams.org/mathscinet-getitem?mr=2031694&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;find_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;81&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1080&lt;/span&gt;

    &lt;span class="n"&gt;cards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2ors,2srd,3opd,3fpd,3fgs,2frs,2fpo,2sps,1opo,1sps,2fro,1fgd&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;sets_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sets_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;find_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;

&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can now empirically determine some facts about the game. I simulated one million 12-card deals (from a fresh deck each time) to see how many sets appear in the initial draw of a game.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;sets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Avg sets:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Min sets:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Max sets:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; sets &lt;/span&gt;&lt;span class="si"&gt;{:.4%}&lt;/span&gt;&lt;span class="s2"&gt; of the time (&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Avg sets: 2.78542
Min sets: 0
Max sets: 12
0 sets 3.2228% of the time (32228 of 1000000)
1 sets 14.5164% of the time (145164 of 1000000)
2 sets 26.1262% of the time (261262 of 1000000)
3 sets 27.2253% of the time (272253 of 1000000)
4 sets 17.9905% of the time (179905 of 1000000)
5 sets 8.0349% of the time (80349 of 1000000)
6 sets 2.3360% of the time (23360 of 1000000)
7 sets 0.4585% of the time (4585 of 1000000)
8 sets 0.0731% of the time (731 of 1000000)
9 sets 0.0129% of the time (129 of 1000000)
10 sets 0.0031% of the time (31 of 1000000)
11 sets 0.0002% of the time (2 of 1000000)
12 sets 0.0001% of the time (1 of 1000000)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To build intuition, we can chart it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;matplotlib&lt;/span&gt; &lt;span class="n"&gt;notebook&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;plt&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="dealing-set" src="http://blog.untrod.com/images/set-stats.png"&gt;&lt;/p&gt;
&lt;p&gt;Neat! The &amp;gt; 10 set case turns out to be quite rare, even though &lt;a href="https://www.setgame.com/sites/default/files/teacherscorner/SETPROOF.pdf"&gt;in theory&lt;/a&gt; (see page 18) 14 sets can exist in a 12-card deal.&lt;/p&gt;
&lt;p&gt;Next up, let's play a full game. The code below accomplishes that.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_game&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;

    &lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="n"&gt;state_log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;sets_exist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

        &lt;span class="n"&gt;state_log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;deck&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;set&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))})&lt;/span&gt;

        &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="c1"&gt;# Don&amp;#39;t put more cards down if a set was drawn from a 15+ card table&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state_log&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_game&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{table}&lt;/span&gt;&lt;span class="s2"&gt; cards on the table. &lt;/span&gt;&lt;span class="si"&gt;{deck}&lt;/span&gt;&lt;span class="s2"&gt; cards in the deck. Set: &lt;/span&gt;&lt;span class="si"&gt;{set}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Game output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;12 cards on the table. 69 cards in the deck. Set: True
12 cards on the table. 66 cards in the deck. Set: True
12 cards on the table. 63 cards in the deck. Set: True
12 cards on the table. 60 cards in the deck. Set: False
15 cards on the table. 57 cards in the deck. Set: True
12 cards on the table. 57 cards in the deck. Set: True
12 cards on the table. 54 cards in the deck. Set: True
12 cards on the table. 51 cards in the deck. Set: True
12 cards on the table. 48 cards in the deck. Set: True
12 cards on the table. 45 cards in the deck. Set: True
12 cards on the table. 42 cards in the deck. Set: True
12 cards on the table. 39 cards in the deck. Set: True
12 cards on the table. 36 cards in the deck. Set: True
12 cards on the table. 33 cards in the deck. Set: True
12 cards on the table. 30 cards in the deck. Set: True
12 cards on the table. 27 cards in the deck. Set: False
15 cards on the table. 24 cards in the deck. Set: True
12 cards on the table. 24 cards in the deck. Set: True
12 cards on the table. 21 cards in the deck. Set: True
12 cards on the table. 18 cards in the deck. Set: True
12 cards on the table. 15 cards in the deck. Set: True
12 cards on the table. 12 cards in the deck. Set: True
12 cards on the table. 9 cards in the deck. Set: True
12 cards on the table. 6 cards in the deck. Set: True
12 cards on the table. 3 cards in the deck. Set: True
12 cards on the table. 0 cards in the deck. Set: True
9 cards on the table. 0 cards in the deck. Set: True
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Up next, let's see how many times in an average game there are no sets in 12 cards, and 15 cards are required (Or 18! Or, in theory at least, 21!). We simulate 10,000 games and keep track of how many times there are different cards on the table (as well as how many cards are on the table at the end game):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="n"&gt;fifteens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;eighteens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;twentyones&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_cards_on_table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_game&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove a final 3 cards if a final set has been found&lt;/span&gt;
    &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;set&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;final_cards_on_table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;fifteens&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;eighteens&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;twentyones&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Final cards on table:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_cards_on_table&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Fifteens: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; times in &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; games.&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fifteens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fifteens&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Eighteens: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; times in &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; games.&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eighteens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eighteens&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Twenty Ones: &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; times in &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; games.&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;twentyones&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;twentyones&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's see:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Final cards on table:
Counter({6: 4853, 9: 4838, 12: 172, 0: 136, 15: 1})
Fifteens: 14327 times in 6696 games.
Eighteens: 144 times in 139 games.
Twenty Ones: 0 times in 0 games.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It appears we'll get a "15" on the table at some point about 2/3rds of the time, and 18 cards only 1.4% of the time. It's also interesting that the vast majority (97%!) of games end with either 6 or 9 cards left on the table, and it's about evenly split between those two outcomes. This surprised me; it's not intuitive to me that the final 12 cards would have about the same odds of containing a set as a final 9 cards. Said differently, given a final 12 cards, it's even odds that it contains exactly 1 set or exactly 2 sets (non-intersecting). You'll also notice that "3" does not appear as endgame scenario. This makes sense - if 78 of 81 cards in the deck have been made into a set, the last three cards must also be a set.&lt;/p&gt;
&lt;p&gt;Lastly, I wondered how useful it would be to continue looking for a set after your opponent calls "Set", but before they have collected the cards from the 12 on the table. We generate 100,000 first draws, find all sets, pick one at random, remove it, and then check if another set exists in the remaining 9 cards.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="n"&gt;sets_in_first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;sets_in_remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;deck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_cards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deck&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sets_in_first&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;picked_s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;picked_s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_sets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sets_in_remainder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Odds a set is still around after the first set is taken: &lt;/span&gt;&lt;span class="si"&gt;{:.2%}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets_in_remainder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sets_in_first&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Odds a set is still around after the first set is taken: 28.28%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It appears that if you can spot a set just after your opponent calls "set", there is a ~28% chance that your late-spotted set will still be there after your opponent has collected their three cards (at least, for the very first deal of the game).&lt;/p&gt;
&lt;p&gt;This is really interesting. The odds that two non-intersecting sets exist in the first twelve cards is about &lt;em&gt;half&lt;/em&gt; the odds that two non-intersecting sets exist in the final 12 cards. That means that, not only should you continue looking for sets as your opponent is collecting cards and dealing new ones, but that that strategy becomes more powerful later in the game (presumably linearly, though I haven't verified).&lt;/p&gt;
&lt;p&gt;For more SET fun and other analyses, Peter Norvig has (of course!) &lt;a href="https://norvig.com/SET.html"&gt;done that a great job&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This code was fun to write, and perhaps you can do some more exploration from here!&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>A Simple Trending Products Recommendation Engine in Python</title><link href="http://blog.untrod.com/2017/02/recommendation-engine-for-trending-products-in-python.md.html" rel="alternate"></link><published>2017-02-04T00:00:00-08:00</published><updated>2017-02-04T00:00:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2017-02-04:/2017/02/recommendation-engine-for-trending-products-in-python.md.html</id><summary type="html">&lt;p&gt;Our product recommendations were boring. I knew that because our
customers told us. When surveyed, the #1 thing they wanted from us was
better product discovery. And looking at the analytics data, I could
see customers clicking through page after page of recommendations,
looking for something new to buy. We …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Our product recommendations were boring. I knew that because our
customers told us. When surveyed, the #1 thing they wanted from us was
better product discovery. And looking at the analytics data, I could
see customers clicking through page after page of recommendations,
looking for something new to buy. We weren't doing a good job
surfacing the back half of our catalog. There was no serendipity.&lt;/p&gt;
&lt;p&gt;One common way of increasing exposure to the long tail of products is
by simply jittering the results at random. But injecting randomness
has two issues: first, you need an awful lot of it to get products
deep in the catalog to bubble up, and second, it breaks the framing of
the recommendations and makes them less credible in the eyes of your
customers.&lt;/p&gt;
&lt;p&gt;What do I mean by 'framing'? Let's look at a famous example from
Yahoo!&lt;/p&gt;
&lt;h2&gt;The Britney Spears Effect.&lt;/h2&gt;
&lt;p&gt;Let's say you're reading about this weekend's upcoming NFL game.
Underneath that article are a bunch of additional articles,
recommended for you by an algorithm. In the early 2000s, it turned out
just about &lt;em&gt;everyone&lt;/em&gt; wanted to read about Britney Spears, whether
they would admit it or not.&lt;/p&gt;
&lt;p&gt;So you get to the bottom of your Super Bowl game preview and it says "You
might also like:" and then shows you an article about Britney and
K-fed. You feel kind of insulted by the algorithm. Yahoo! thinks I
want to read about Britney Spears??&lt;/p&gt;
&lt;p&gt;But instead, what if said "Other people who read this article
read:". Now...huh...ok - I'll click. The framing gives me permission
to click. This stuff matters!&lt;/p&gt;
&lt;p&gt;Just like a good catcher can frame a on-the-margin baseball pitch for
an umpire, showing product recommendations on a website in the right
context puts customers in the right mood to buy or click.&lt;/p&gt;
&lt;p&gt;"Recommended for you" -- ugh. So the website thinks it knows me, eh?
How about this instead:&lt;/p&gt;
&lt;p&gt;"Households like yours frequently buy"&lt;/p&gt;
&lt;p&gt;Now I have context. Now I understand. This isn't a retailer shoving
products in front of my face, it's a helpful assemblage of products
that customers just like me found useful. Chock-full of social proof!&lt;/p&gt;
&lt;h2&gt;Finding Some Plausible Serendipity&lt;/h2&gt;
&lt;p&gt;After an awesome brainstorming session with one of our investors, Paul
Martino from &lt;a href="http://bullpencap.com/"&gt;Bullpen Capital&lt;/a&gt;, we came up
with the idea of a trending products algorithm. We'll take all of the
add-to-cart actions every day, and find products that are trending
upwards. Sometimes, of course, this will just reflect the activities
of our marketing department (promoting a product in an email, for
instance, would cause it to trend), but with proper standardization it
should also highlight newness, trending search terms, and other
serendipitous reasons a product might be of interest. It's also easier
for slower-moving products to make sudden gains in popularity so
should get some of those long-tail products to the surface.&lt;/p&gt;
&lt;h2&gt;Implementing a Trending Products Engine&lt;/h2&gt;
&lt;p&gt;First, let's get our add-to-cart data. From our database, this is
relatively simple; we track the creation time of every cart-product
(we call it a 'shipment item') so we can just extract this using
SQL. I've taken the last 20 days of cart data so we can see some
trends (though really only a few days of data is needed to determine
what's trending):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ss"&gt;&amp;quot;age&amp;quot;&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;product_variant&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="k"&gt;INNER&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;schedule_shipmentitem&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;variant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;20 DAYS&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;si&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I've simplified the above a bit (the production version has some
subtleties around active products, paid customers, the circumstances
in which the product was added, etc), but the shape of the resulting
data is dead simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;id  age count
14  -20 22
14  -19 158
14  -18 94
14  -17 52
14  -16 56
14  -15 56
14  -14 52
14  -13 100
14  -12 109
14  -11 151
14  -10 124
14  -9  123
14  -8  58
14  -7  64
14  -6  114
14  -5  93
14  -4  112
14  -3  87
14  -2  81
14  -1  19
15  -20 16
...
15  -1  30
16  -20 403
...
16  -1  842
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each row represents the number of cart adds for a particular product
on a particular day in the past 20 days. I use 'age' as -20 (20 days
ago) to -1 (yesterday) so that, when visualizing the data, it reads
left-to-right, past-to-present, intuitively.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://blog.untrod.com/files/sample-cart-add-data.csv"&gt;Here's sample data&lt;/a&gt; for
100 random products from our database. I'm anonymized both the product
IDs and the cart-adds in such a way that, when standardized, the
results are completely real, but the individual data points don't
represent our actual business.&lt;/p&gt;
&lt;h2&gt;Basic Approach&lt;/h2&gt;
&lt;p&gt;Before we dive into the code, let's outline the basic approach by
visualizing the data. All the code for each intermediate step, and the
visualizations, is included and explained later.&lt;/p&gt;
&lt;p&gt;Here's the add-to-carts for product 542, from the sample dataset:&lt;/p&gt;
&lt;p&gt;&lt;img alt="basic-trend" src="http://blog.untrod.com/images/trending/trend.png"&gt;&lt;/p&gt;
&lt;p&gt;The first thing we'll do is add a low-pass filter (a smoothing
function) so daily fluctuations are attentuated.&lt;/p&gt;
&lt;p&gt;&lt;img alt="smoothed" src="http://blog.untrod.com/images/trending/smoothed.png"&gt;&lt;/p&gt;
&lt;p&gt;Then we'll standardize the Y-axis, so popular products are comparable
with less popular products. Note the change in the Y-axis values.&lt;/p&gt;
&lt;p&gt;&lt;img alt="standardized" src="http://blog.untrod.com/images/trending/standardized.png"&gt;&lt;/p&gt;
&lt;p&gt;Last, we'll calculate the slopes of each line segment of the smoothed
trend.&lt;/p&gt;
&lt;p&gt;&lt;img alt="slopes" src="http://blog.untrod.com/images/trending/slopes.png"&gt;&lt;/p&gt;
&lt;p&gt;Our algorithm will perform these steps (in memory, of course, not
visually) for each product in the dataset and then simply return the
products with the greatest slope values in the past day, e.g. the max
values of the red line at t=-1.&lt;/p&gt;
&lt;h2&gt;The Code&lt;/h2&gt;
&lt;p&gt;Let's get into it! You can run all of the code in this post via a
Python 2 Jupyter notebook.&lt;/p&gt;
&lt;p&gt;Here's the code to produce the first chart (simply visualizing the
trend). Just like we built up the charts, we'll build from this code
to create the final algorithm.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;plt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# Read the data into a Pandas dataframe&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sample-cart-add-data.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Group by ID &amp;amp; Age&lt;/span&gt;
&lt;span class="n"&gt;cart_adds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pivot_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;count&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;age&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;542&lt;/span&gt;
&lt;span class="n"&gt;trend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart_adds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Cart Adds&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bbox_to_anchor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;borderaxespad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It doesn't get much simpler. I use the pandas pivot_table function to
create an index of both product IDs and the 'age' dimension, which
just makes it easy to select the data I want later.&lt;/p&gt;
&lt;h2&gt;Smoothing&lt;/h2&gt;
&lt;p&gt;Let's write the smoothing function and add it to the chart:&lt;/p&gt;
&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="c1"&gt;# Generate data points &amp;#39;outside&amp;#39; of x on either side to ensure&lt;/span&gt;
    &lt;span class="c1"&gt;# the smoothing window can operate everywhere&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

    &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;same&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;smoothed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# trim away the excess data&lt;/span&gt;

&lt;span class="n"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hamming&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smoothed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Smoothed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;p&gt;This function merits an explanation. First, it's taken more-or-less
from the &lt;a href="http://scipy-cookbook.readthedocs.io/items/SignalSmooth.html"&gt;SciPy Cookbook&lt;/a&gt;,
but modified to be...less weird.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;smooth&lt;/code&gt; function takes a 'window' of weights, defined in this
case by the
&lt;a href="https://en.wikipedia.org/wiki/Window_function#Hamming_window"&gt;Hamming Window&lt;/a&gt;,
and 'moves' it across the original data, weighting adjacent data
points according to the window weights.&lt;/p&gt;
&lt;p&gt;Numpy provides a bunch of windows (Hamming, Hanning, Blackman, etc.)
and you can get a feel for them at the command line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; print np.hamming(7)
[ 0.08  0.31  0.77  1.    0.77  0.31  0.08]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That 'window' will be moved over the data set ('convolved') to create
a new, smoothed set of data. This is just a very simple low-pass filter.&lt;/p&gt;
&lt;p&gt;Lines 5-7 invert and mirror the first few and last few data points in
the original series so that the window can still 'fit', even at the
edge data points. This might seem a little odd, since at the end of
the day we are only going to care about the final data point to
determine our trending products. You might think we'd prefer to use a
smoothing function that only examines historical data. But because the
interpolation just mirrors the trailing data as it approaches the
forward edge, there's ultimately no net effect on the result.&lt;/p&gt;
&lt;h2&gt;Standardization&lt;/h2&gt;
&lt;p&gt;We need to compare products that average, for instnace, 10 cart-adds
per day to products that average hundreds or thousands. To solve this
problem, we standardize the data by dividing by the Interquartile
Range (IQR):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;standardize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;

&lt;span class="n"&gt;smoothed_std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;standardize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smoothed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smoothed_std&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I also subtract the median so that the series more-or-less centers
around 0, rather than 1. Note that this is &lt;em&gt;standardization&lt;/em&gt; not
&lt;em&gt;normalization&lt;/em&gt;, the difference being that normalization strictly
bounds the value in the series between a known range (typically 0 and
1), whereas standardization just puts everything onto the same scale.&lt;/p&gt;
&lt;p&gt;There are plenty of ways of standardizing data; this one is plenty
robust and easy to implement.&lt;/p&gt;
&lt;h2&gt;Slopes&lt;/h2&gt;
&lt;p&gt;Really simple! To find the slope of the smoothed, standardized series
at every point, just take a copy of the series, offset it by 1, and
subtract. Visually, for some example data:&lt;/p&gt;
&lt;p&gt;&lt;img alt="slopes" src="http://blog.untrod.com/images/trending/slope-calc.png"&gt;&lt;/p&gt;
&lt;p&gt;And in code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;slopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smoothed_std&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;smoothed_std&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Boom! That was easy.&lt;/p&gt;
&lt;h2&gt;Putting it all together&lt;/h2&gt;
&lt;p&gt;Now we just need to repeat all of that, for every product, and find
the products with the max slope value at the most recent time step.&lt;/p&gt;
&lt;p&gt;The final implementation is below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;operator&lt;/span&gt;

&lt;span class="n"&gt;SMOOTHING_WINDOW_FUNCTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hamming&lt;/span&gt;
&lt;span class="n"&gt;SMOOTHING_WINDOW_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sample-cart-add-data.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;age&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;trends&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pivot_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;count&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;age&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;trend_snap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;trend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trends&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SMOOTHING_WINDOW_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SMOOTHING_WINDOW_FUNCTION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;nsmoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;standardize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;smoothed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;slopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nsmoothed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;nsmoothed&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# I blend in the previous slope as well, to stabalize things a bit and&lt;/span&gt;
        &lt;span class="c1"&gt;# give a boost to things that have been trending for more than 1 day&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;trend_snap&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slopes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;slopes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trend_snap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;itemgetter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;smooth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;smoothed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;same&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;smoothed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;standardize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;iqr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;iqr&lt;/span&gt;


&lt;span class="n"&gt;trending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Top 5 trending products:&amp;quot;&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trending&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Product &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; (score: &lt;/span&gt;&lt;span class="si"&gt;%2.2f&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And the result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Top &lt;span class="m"&gt;5&lt;/span&gt; trending products:
Product &lt;span class="m"&gt;103&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;score: &lt;span class="m"&gt;1&lt;/span&gt;.31&lt;span class="o"&gt;)&lt;/span&gt;
Product &lt;span class="m"&gt;573&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;score: &lt;span class="m"&gt;1&lt;/span&gt;.25&lt;span class="o"&gt;)&lt;/span&gt;
Product &lt;span class="m"&gt;442&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;score: &lt;span class="m"&gt;1&lt;/span&gt;.01&lt;span class="o"&gt;)&lt;/span&gt;
Product &lt;span class="m"&gt;753&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;score: &lt;span class="m"&gt;0&lt;/span&gt;.78&lt;span class="o"&gt;)&lt;/span&gt;
Product &lt;span class="m"&gt;738&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;score: &lt;span class="m"&gt;0&lt;/span&gt;.66&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's the core of the algorithm. It's now in production, performing
well against our existing algorithms. We have a few additional pieces
we're putting in place to goose the performance further:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Throwing away any results from wildly unpopular products.
   Otherwise, products that fluctuate around 1-5 cart-adds per day too
   easily appear in the results just by jumping to 10+ adds for one
   day.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Weighting products so that a product that jumps from an average of
   500 adds/day to 600 adds/day has a chance to trend alongside a
   product that jumped from 20 to 40.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;There is weirdly little material out there about trending algorithms -
and it's entirely possible (likely, even) that others have more
sophisticated techniques that yield better results.&lt;/p&gt;
&lt;p&gt;But for Grove, this hits all the marks: explicable, serendipitous, and
gets more clicks than anything other product feed we've put in front
of our customers.&lt;/p&gt;</content><category term="Data Science"></category></entry><entry><title>How to Buy Expensive Software Part 1: Vendors &amp; Demos</title><link href="http://blog.untrod.com/2016/11/negotiate-and-buy-saas-software-part-1-picking-a-vendor.html" rel="alternate"></link><published>2016-11-20T00:00:00-08:00</published><updated>2016-11-20T00:00:00-08:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-11-20:/2016/11/negotiate-and-buy-saas-software-part-1-picking-a-vendor.html</id><summary type="html">&lt;p&gt;Some days it seems like all I'm good for is buying other companies'
software.&lt;/p&gt;
&lt;p&gt;It's a little ridiculous. I get 5-10 emails a day pitching some SaaS
solution. You probably get them too. Recognize any of these subject
lines?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Christopher, do you have 15 minutes to connect about my previous …&lt;/em&gt;&lt;/li&gt;&lt;/ul&gt;</summary><content type="html">&lt;p&gt;Some days it seems like all I'm good for is buying other companies'
software.&lt;/p&gt;
&lt;p&gt;It's a little ridiculous. I get 5-10 emails a day pitching some SaaS
solution. You probably get them too. Recognize any of these subject
lines?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Christopher, do you have 15 minutes to connect about my previous note?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Try SaaSly for a week - Get $200&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What do Lyft, Hubspot and Instacart all have in common?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;wrong person?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Meeting Request - EXCLUSIVE&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My blood pressure went up 10% just copy-pasting those. On the other
hand, I can't fault these sales folks because, once in a while, I'm
actually in market and end up having to buy one of these things.&lt;/p&gt;
&lt;p&gt;And that's what this is about -- buying expensive software.&lt;/p&gt;
&lt;h2&gt;What's "expensive"?&lt;/h2&gt;
&lt;p&gt;For our purposes, "expensive" is anything that costs enough money that
the vendor has dedicated sales people who will actually schedule
meetings with you, answer your questions, prepare demos, and negotiate
pricing. Others have written about this before, but basically there is
some threshold where prices jump from, like, $399/mo to $1500/mo. It's
sort of hard to find software that costs
$900/mo&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt; because I'm not going to spend that
much money without demos and talking to a sales rep, but it's not
really a large enough sale to support that kind of sales
infrastructure either&lt;sup&gt;&lt;a href="#footnote2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;For that reason, most real B2B SaaS solutions start somewhere in the
$2000/mo range. If you're a growing company, at some point you'll
suddenly find that every department in the company wants to buy SaaS
products that are each $2000/mo. Yikes.&lt;/p&gt;
&lt;p&gt;I'll walk you how these deals come together, what to expect at each
stage of the purchasing process, help you ask the right questions, and
ultimately negotiate price, while still remaining on great terms with
your vendor.&lt;/p&gt;
&lt;h2&gt;Entering the Market&lt;/h2&gt;
&lt;p&gt;So let's pretend you're in market for a business intelligence tool. Or
a CRM. Or an inventory management solution. Or an email marketing
system. Or possibly you're in market for all of these and SalesForce
has promised that The Force.com Platform (r)(tm) can do all of this,
plus get you out of unpaid parking tickets, and you will have lots of
Success and not too much Software and it's going to be a lovely,
platformy day.&lt;/p&gt;
&lt;p&gt;The first thing you have to do is pick a couple of vendors. Even if
you have a strong idea about who will be the best fit, I always like
looking at two vendors, if possible. The competitive dynamics of the
deal will give you some negotiating leverage later on, and even if the
'other' vendor ends up being weak, it'll give you just that much more
conviction about the solution you picked. Anywhere between 2 and 4
vendors is great, and you can always drop unpromising ones at any
point through the process.&lt;/p&gt;
&lt;p&gt;Got a couple in mind? Great! If you don't have a contact there
already, just fill out a lead form online and I promise you will have
a sales rep at your doorstep in no time.&lt;/p&gt;
&lt;h2&gt;Meet You New Best Friend&lt;/h2&gt;
&lt;p&gt;This is important to remember: &lt;strong&gt;the sales rep is on your side&lt;/strong&gt;. This
is someone who is very interested in both getting this deal done
(therefore at a price you can afford, and with the features you want)
and in making sure the solution is actually a good fit (because all
sorts of things can go sideways after purchase, and that has real
financial consequences for the rep).&lt;/p&gt;
&lt;p&gt;Sales reps are your friends. They may frustrate you with their
inability to answer technical questions, or give you a little bit of
the runaround on how a particular feature works, but they are on your
side. So be on theirs. When it comes time to put the screws to the
vendor on pricing, it's your rep who will go to bat with their
managers to get your pricing approved. So be friendly!&lt;/p&gt;
&lt;h2&gt;The First Phone Call&lt;/h2&gt;
&lt;p&gt;You've reached out and made contact with your sales rep via email. It's
looking good and you are now a warm lead. The next step is an initial
call.&lt;/p&gt;
&lt;p&gt;It's somewhat unlikely you will actually get a demo of the software in
this first call. Totally annoying, yes, but normal. Deal with it. Your
rep is first going to want to understand your goals and your company,
so she can put together a relevant demo later on. You'll definitely be
asked to explain your role in the company. Regardless of whether I'm
the sole decision-maker or just helping another department make a
purchase, I always make sure the rep knows that I am necessary, but
not sufficient, for getting this deal done. This puts me in a position
where my concerns and questions have to be taken seriously, but also
creates the propsect of additional stakeholders, who I can use as 'the
bad guy' later in negotiations, and to buy myself time if I get in a
jam.&lt;/p&gt;
&lt;p&gt;Now, back to that first call: you have some important diligence of
your own that you will need to perform.&lt;/p&gt;
&lt;p&gt;But first, you will have to battle your way through the Boring Vendor
PowerPoint of Irrelevance. After a few minutes of standard "can you
see my screen?" GoToMeeting formalities, you'll probably get a
slideshow like this:&lt;/p&gt;
&lt;h4&gt;Slide 1:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GIANT COMPANY LOGO&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Lots of little logos of prominent customers&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Trusted by industry leaders"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Depending on the sophistication of the sales org, they may have
  taken the dramatic step of &lt;em&gt;including your company's logo&lt;/em&gt;. +1
  vendor points if so.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Slide 2:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;LOTS OF STATS ABOUT RETURN ON INVESTMENT and HOW YOUR COMPANY IS
  GOING TO GET SUPER LASER NINJA POWERS WHEN YOU BUY THIS THING&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Slide 3:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;List of modules and features. 90% chance the word "proprietary"
  appears in at least one of the bullet points.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Slide 4:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;I'm not really sure, I've generally tuned out by now. There are
  definitely a few more slides though.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I feel bad zoning out, but unless there's some buying process I'm
unaware of where, like, you end up on sales calls based on that fact
that you both swiped right on each other's LinkedIn profiles, you've
almost certainly read this information on their website. I just never
learn much via cookie-cutter PowerPoint presentations, other than
slide transitions look the least crappy when screen
sharing&lt;sup&gt;&lt;a href="#footnote2"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I save my detailed solution and product questions for demos (which
come later) and my approach in the initial call is to just kind of
'uh-huh' my way through the slides as quickly as possible so I can get
to the real goal of the first meeting:&lt;/p&gt;
&lt;h2&gt;Vetting the Vendor&lt;/h2&gt;
&lt;p&gt;You're about to spend a bunch of money, so make sure you understand
who you are partnering with. Here are the questions I like to ask in
the first call (when relevant), and why I ask them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How big is your largest customer? How small is your smallest?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Like Goldilocks, I want to find a vendor that serves customers
  my size. If I'm a tiny customer, I won't be able to get
  attention when I need it. And I don't want to be the biggest
  customer, and the first to run into scaling challenges.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How many customers do you have? How many engineers? Are you
  profitable?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;How big is this company? Do they serve lots of companies just
  like mine? Can they fix problems quickly? Are they a startup
  that might suddenly go out of business?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Where are you located? What's the support model? Is there phone
  support?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Especailly important if this is critical line-of-business
  software! I can deal with my data viz tool going down for an
  hour, but my warehouses just go idle if my inventory management
  tool is down. So what I really want to know here is: when the
  shit hits the fan, can I talk to a live human being &lt;em&gt;who can fix
  the problem&lt;/em&gt;? I've had vendors with great phone support, but
  their engineers are all in Asia, so all they could offer was
  sympathy until 10pm when folks started waking up.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's your target customer? What parts of the market are important
  to you in the next 2 years?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;It may be the case that you are a pretty good fit for this
  vendor today, but their roadmap has them focusing down-market on
  smaller companies in the next 2 years, and the features that are
  getting built aren't going to be ones you value.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Will I be able to do reference calls with similar customers?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Many vendors won't actually set up reference calls until much
  later in the process (I can't blame them -- you can't go out
  hassling your customers for a call every time a lead asks this
  question) but it's important to know you'll be able to do it
  later. If the answer is "We don't do references", that's a major
  red flag for me.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give me an estimate: How long will this take to implement?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;I've heard vendors tell me "30 days" but it turns out they are
  running a 45-day project backlog, so total time to launch is
  much longer. If there's a big range, you can ask what's typical,
  and then "What's the fastest you've ever seen a company
  implement?". You know best whether you're going to be typical,
  or fast. This question is also a great gut-check for you own
  knowledge; if you had "a week from purchase to go-live" in your
  head, and then vendor is saying 90 days is typical, maybe you
  don't totally understand what you're getting into.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give me an estimate: How much is this going to cost?&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;This will be the first and last time you talk about price until
  the end of the process. I try to get a sense of first-year total
  cost of ownership (TCO) by asking about software costs,
  implementation costs, and asking if there are any other types of
  costs I should be aware of. I generally say something like:
  "Based on what you know about what I'm trying to accomplish,
  what kind of costs do companies like mine generally see in the
  first year? Giving me a range is fine, and I won't hold you to
  it. I just want to make sure we're in the same ball park."&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There aren't right and wrong answers to the above, but trust your
instincts. You really are going to be partners in making this work, at
least through the sales and implementation period. If the vendor seems
good, but the sales rep seems like an idiot, that's a red flag. If the
rep is great, but the product story seems thin, that's a red
flag. You're about to spend a lot of money, with a contractual
committment that will be hard to unwind. Take your time, be thorough,
and build conviction.&lt;/p&gt;
&lt;h2&gt;Getting the demo&lt;/h2&gt;
&lt;p&gt;To get the most out of a demo, I prepare a list of requirements and
questions in advance, and drill deep into the weeds during the
demo. Don't be afraid to take the technical sales person off
script. This serves two functions. First, it pressure-tests the system
a bit. Is it flexible? Is it intuitive? Are the UX and data metaphors
it uses ones that make sense to you?&lt;/p&gt;
&lt;p&gt;Second, you can verify that the capabilities advertised actually work
the way you need them to work.&lt;/p&gt;
&lt;p&gt;I've never seen intentional dishonesty during a demo, but it's easy to
interpret a question in a way that allows them to say "yes, we do
that", but might not actually work for you. A real example: I was
buying an inventory management system and asked "do you support EDI?"
(EDI is a standard communication protocal for tracking the status of
orders). The vendor said yes, and they do support it, but it turned
out only for outbound orders, not inbound, which is where I needed
it. You owe it to your company to be tenacious and dig in.&lt;/p&gt;
&lt;p&gt;My last tip for demos is to have your screenshot tool of choice
configured and ready to rock. These things can move fast so it's great
to be able to save screenshots of each part of the demo to review
later, and to send to colleagues who might also have questions. If
you're getting demos of multiple solutions, this is especially helpful
as they tend to run together in your memory after a while.&lt;/p&gt;
&lt;h2&gt;What's next?&lt;/h2&gt;
&lt;p&gt;You're honing in on a decision - congratulations! Hopefully you have
at least two vendors that look OK (you only need one, but having two
is great for negotiation purposes). Next up you'll request a pricing
quote, a statement of work (a scope), and a contract. Stay tuned for
part 2 - how to negotiate price, and get the deal done.&lt;/p&gt;
&lt;h2&gt;Footnotes&lt;/h2&gt;
&lt;p&gt;&lt;a name="footnote1"&gt;1&lt;/a&gt;: The exceptions are things that start of cheap
and get expensive based on usage.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote2"&gt;2&lt;/a&gt;: I think there may be an opportunity for a
start-up that doesn't really innovate on product but instead makes,
e.g., a perfectly decent mid-market CRM product and innovates instead
on the sales process by somehow avoiding it entirely, and charging,
like, $900/mo, which would radically undercut their competitors.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote3"&gt;3&lt;/a&gt;: The answer is: They all look terrible. Save
the special effects budget for something else.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Photography &amp; Journalism with Darcy Padilla &amp; Julian Cox - Opening Remarks</title><link href="http://blog.untrod.com/2016/11/photography-journalism-remarks-darcy-padilla-danny-lyon-julian-cox.html" rel="alternate"></link><published>2016-11-19T00:00:00-08:00</published><updated>2016-11-19T00:00:00-08:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-11-19:/2016/11/photography-journalism-remarks-darcy-padilla-danny-lyon-julian-cox.html</id><summary type="html">&lt;p&gt;On November 17th, I chaired an event at the de Young Museum related to
the opening of the exhibit
&lt;a href="https://deyoung.famsf.org/exhibitions/danny-lyon-message-future"&gt;Danny Lyon: Message to the Future&lt;/a&gt;. It's
there through April 30th, 2017 and I encourage anyone in San Francisco
during its run to see it. Danny Lyon is a remarkable photographer …&lt;/p&gt;</summary><content type="html">&lt;p&gt;On November 17th, I chaired an event at the de Young Museum related to
the opening of the exhibit
&lt;a href="https://deyoung.famsf.org/exhibitions/danny-lyon-message-future"&gt;Danny Lyon: Message to the Future&lt;/a&gt;. It's
there through April 30th, 2017 and I encourage anyone in San Francisco
during its run to see it. Danny Lyon is a remarkable photographer and
artist, with a long history of nosing into important social themes, at
just the right time. The work of Bay Area photographer and
journalist &lt;a href="http://www.darcypadilla.com/"&gt;Darcy Padilla&lt;/a&gt; is closely
related. She has spent her career in similar pursuits, with haunting
and memorable results.&lt;/p&gt;
&lt;p&gt;Through ArtPoint, the young patron's group affiliated with the
museums, we hosted a conversation between Darcy and Julian Cox, the
curator who put together the Danny Lyon exhibit, in collaboration with
the artist, over the past few years. The discussion between Julian
and Darcy focused on her work, and the broader concerns of socially
conscious photo-journalism. My opening remarks are below.&lt;/p&gt;
&lt;h3&gt;Remarks&lt;/h3&gt;
&lt;p&gt;The tangled intimacy of artist and subject is what grabbed me the
first time I saw Danny Lyon's work, and Darcy Padilla's.&lt;/p&gt;
&lt;p&gt;With Lyon, it was a photo from The Bikeriders. A young man, about 20,
with a pock-marked face, leering out at me -- a face strikingly similar to
my father's, and wearing a motorcycle club jacket that my dad would
have been jealous of, having flirted with, but never quite fully
embraced, his own rebellious side.&lt;/p&gt;
&lt;p&gt;And with Darcy, it was The Julie Project, the heart-wrenching,
decade-and-a-half-long photo-journalism story of Julie, a
mother trapped in relentless poverty, as she lived and died with
drugs, AIDS, and abuse. I came across The Julie Project at work, which
I would not recommend, as my colleagues are still worried about what
caused me to sob at my desk in the middle of a work day.&lt;/p&gt;
&lt;p&gt;The images from Darcy, an award-winning photojournalist and
our guest here tonight, and Danny Lyon, in whose work you will find
many parallels, and you'll have the opportunity to view after this
program, these images are full to the brim with empathy.&lt;/p&gt;
&lt;p&gt;These are images of immediate proximity. Of immediate understanding --
to people and situations otherwise hard to see, on the margins of
society.&lt;/p&gt;
&lt;p&gt;Darcy &amp;amp; Danny have spent their careers documenting people, stories,
and narratives that are simply not in our usual line of sight.&lt;/p&gt;
&lt;p&gt;They have both worked extensively in prisons, surfacing the
intentionally and knowingly hidden away. And both have documented
systemic poverty and disenfranchisement.&lt;/p&gt;
&lt;p&gt;It is hard work to see from the perspective of others. It takes effort
and exertion and intention. These artists do this work on our behalf
so we can transmute from stories that would otherwise simply fade from
our collective consciousness.&lt;/p&gt;
&lt;p&gt;Tonight, we have a unique opportunity to learn more. Julian Cox, chief
curator of the Fine Arts Museums of San Francisco, will speak with
Darcy about present day photo-journalism, and her work. Afterwards,
please explore the exhibit Danny Lyon: Message to the Future, which
Julian has organized, and is the first comprehensive retrospective of
Lyon's work. It will remain open until 10pm, in the far galleries
on this floor.&lt;/p&gt;
&lt;p&gt;On behalf of ArtPoint, and the Fine Arts Museums of San Francisco, we
welcome you, our audience, and Julian and Darcy. Thank you, and enjoy.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Actually Understanding Timezones in PostgreSQL</title><link href="http://blog.untrod.com/2016/08/actually-understanding-timezones-in-postgresql.html" rel="alternate"></link><published>2016-08-12T12:09:00-07:00</published><updated>2016-08-12T12:09:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2016-08-12:/2016/08/actually-understanding-timezones-in-postgresql.html</id><summary type="html">&lt;p&gt;Timezones are the worst. We should all just live in UTC, reasonable
sunset hours be damned. And learning about timezone implementation
details is incredibly boring and esoteric&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
so most developers are not inclined to figure out what the hell is
actually going on until they really need to. Like …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Timezones are the worst. We should all just live in UTC, reasonable
sunset hours be damned. And learning about timezone implementation
details is incredibly boring and esoteric&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
so most developers are not inclined to figure out what the hell is
actually going on until they really need to. Like me, yesterday, when
my slightly-imprecise understanding of how PostgreSQL handles
timezones ended with a nervous breakdown at my desk, and the marketing
department looking very concerned.&lt;/p&gt;
&lt;p&gt;My query results seemed
&lt;em&gt;impossible&lt;/em&gt;! Down was up, up was down, Pacific Time suddenly had no
meaning, and I imagined a bunch of National Time Service officials in
Greenwhich sniggering at me.&lt;/p&gt;
&lt;p&gt;But it turns out all is indeed right with the world, everything makes
sense, and I totally know what's going on, now. Follow along (you just
need the ability to run SQL on PostgreSQL) and join me in the basking
glow of timezone comprehension.&lt;/p&gt;
&lt;p&gt;If you make it all the way through, I promise you'll know enough to
puzzle out any Postgres timezone question you may have - with the
exception of my mystery question at the end, which is totally insane
and inexplicable.&lt;/p&gt;
&lt;h2&gt;Easy Sample Data&lt;/h2&gt;
&lt;p&gt;To start, let's make a test table. Easy.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ts_naked&lt;/span&gt; &lt;span class="k"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;without&lt;/span&gt; &lt;span class="k"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;timestamp&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;time&lt;/span&gt; &lt;span class="k"&gt;zone&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One tz-unaware timestamp field, and one that knows about
timezones. We'll unpack what "knows about timezones" means shortly.&lt;/p&gt;
&lt;p&gt;And I'll also assume you have a PostgreSQL server, with a default of
UTC time, via the timezone parameter into postgresql.conf being set
like this: &lt;code&gt;timezone = 'UTC'&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Insertin' Stuff&lt;/h2&gt;
&lt;p&gt;Let the fun begin! Let's first insert some data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts_naked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2016-08-12 10:22:31.949271-07&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="k"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2016-08-12 10:22:31.949271-07&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that &lt;em&gt;both&lt;/em&gt; of the inserted values have a -07 at the end,
indicating a -7 hour offset from UTC, which, given that I'm writing
this in California (normally a -08 offset), in August, means that
daylight savings time is in effect, thus changing my local UTC offset
by an hour, to UTC-07. Phew. Ok. We're doing great.&lt;/p&gt;
&lt;p&gt;But wait a minute! I just inserted an offset time zone into BOTH my
'naked' timestamp field (that doesn't know about timezones) and the
tz-aware field. So what happens when I get the data back out?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;          ts_naked          |             ts_tz&lt;/span&gt;
&lt;span class="go"&gt;----------------------------+-------------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 10:22:31.949271 | 2016-08-12 17:22:31.949271+00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Hm, ok. So basically the naked timezone just took the inserted
timestamp at face value and wrote it on in. It completely ignored the
-07 at the end. And yep, that makes sense. The
&lt;a href="https://www.postgresql.org/docs/9.1/static/datatype-datetime.html#AEN5788"&gt;PostgreSQL docs&lt;/a&gt;
confirm it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PostgreSQL never examines the content of a literal string before
determining its type, and therefore will treat both of the above as
timestamp without time zone. To ensure that a literal is treated as
timestamp with time zone, give it the correct explicit type:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's not enough to provide the -07 at the end, you need to actually
say TIMESTAMP WITH TIME ZONE. But also note that postgres &lt;em&gt;did not
save&lt;/em&gt; the timezone information for the tz-aware field; this is a
really important distinction. Postgres TIMESTAMP WITH TIME ZONE fields
&lt;em&gt;do not actually store any TZ info&lt;/em&gt;, they just expect TZ info to be
&lt;em&gt;present&lt;/em&gt; when the fields are written to. Under the covers, Postgres
converts it to UTC and internally says "yep, great, I am confident that I am
storing this timestamp, normalized to UTC". With that
confidence, Postgres can now perform any timezone conversion you'd
like. But there's no way to ever retrieve the fact that I originally
sent down a timestamp with a UTC-07 offset; that information is gone
forever.&lt;/p&gt;
&lt;h2&gt;Readin' Stuff&lt;/h2&gt;
&lt;p&gt;Right, so we have data, let's get it back out and see what happens.&lt;/p&gt;
&lt;p&gt;First, let's deal with our stupid ts_naked field. It's pretty easy
to understand. When we wrote to it, it interpreted the timestamp as
literal, and basically just burned it straight into the database. So,
no surprise, bad shit happens when you try to use it with timezones:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_naked&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;America/Los_Angeles&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;           timezone&lt;/span&gt;
&lt;span class="go"&gt;-------------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 17:22:31.949271+00&lt;/span&gt;

&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_naked&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;UTC&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;           timezone&lt;/span&gt;
&lt;span class="go"&gt;-------------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 10:22:31.949271+00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yeah...that's pretty wacky. Postgres has no idea wtf time you actually
stored, so it just treats it as whatever the current server timezone
is (UTC in our case, from setting timezone in the postgresql.conf
file), applies the offsets you requested, and kinda throws up its
hands and gives up by returning a timestamp without an offset (as
indicated by the +00). So that's not very useful. Don't use timestamps
without time zones in PostgreSQL. There, easy.&lt;/p&gt;
&lt;p&gt;It's also worth noting at this point that Postgres will let you apply
AT TIME ZONE conversions to &lt;em&gt;date&lt;/em&gt; fields. I think this is nuts and
Postgres should at least throw up a warning, as it's very weird to
request a &lt;em&gt;date&lt;/em&gt; at a specific timezone and get a totally different
date. Thursday August 18th is Thursday August 18th in every time zone;
but instead Postgres will sneakily cast it to a datetime, assume
midnight, do the TZ conversion, and return a datetime. Check it out:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;July 1, 2015&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;America/Los_Angeles&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;  timezone&lt;/span&gt;
&lt;span class="go"&gt;------------&lt;/span&gt;
&lt;span class="go"&gt; 2015-06-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I mean...come on.&lt;/p&gt;
&lt;p&gt;Ok, enough with things you &lt;em&gt;shouldn't&lt;/em&gt; do, let's focus on the
interesting one. It behaves like you'd hope...for now.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;UTC&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;          timezone&lt;/span&gt;
&lt;span class="go"&gt;----------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 17:22:31.949271&lt;/span&gt;


&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;America/Los_Angeles&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;          timezone&lt;/span&gt;
&lt;span class="go"&gt;----------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 10:22:31.949271&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great. We can select our data, and request it in any time zone we
want, and nothing weird happens. Although...there is one
surprise. Note that we actually did not get any timezone information
back from these queries; there's not offset at the end of the
timestamps. Postgres returned a TIMESTAMP WITHOUT TIME ZONE!&lt;/p&gt;
&lt;p&gt;By requesting the data in a particular timezone, Postgres says "you
know the offset, since you just asked for it, so I don't need to give
it you". There's no offset specified at the end of the timestamp. I
don't particularly like this; we're returning ambiguous information
over the wire that we might want later on. Let's try something else.&lt;/p&gt;
&lt;p&gt;Recall that a plain-old query will in fact give us a TZ-aware
timestamp, in the server time:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;             ts_tz&lt;/span&gt;
&lt;span class="go"&gt;-------------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 17:22:31.949271+00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But check this out!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;America/Los_Angeles&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;             ts_tz&lt;/span&gt;
&lt;span class="go"&gt;-------------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 10:22:31.949271-07&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Whaaaaaa??! It turns out that &lt;em&gt;every Postgres session has its own time
zone&lt;/em&gt;. Whoa. The 'timezone' setting that we specified in the
postgresql.conf file actually just provides a default timezone value
to every session, nothing more than that.&lt;/p&gt;
&lt;p&gt;And this is where it's easy to get very surprised. For instance, when
Django connects to Postgres, it
'&lt;a href="https://github.com/django/django/blob/master/django/db/backends/postgresql/base.py#L195"&gt;ensures timezone&lt;/a&gt;'
and slaps the application time zone onto the psycopg2 connection so
you get back timestamps in the same timezone you put them in (assuming
you are inserting local times).&lt;/p&gt;
&lt;p&gt;That's clever, but can lead to incredibly confusion if you are trying
to compare results from the same database, but via 2 different
connections, which are set to two different time zones. You can always
check the current session TZ via &lt;code&gt;SELECT
current_setting('TIMEZONE');&lt;/code&gt; or &lt;code&gt;show timezone;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Isn't this fun??&lt;/p&gt;
&lt;h2&gt;Offsettin' Stuff&lt;/h2&gt;
&lt;p&gt;For extra confusion, Postgres will happily let you do this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;ts_tz&lt;/span&gt; &lt;span class="k"&gt;AT&lt;/span&gt; &lt;span class="nb"&gt;TIME&lt;/span&gt; &lt;span class="k"&gt;ZONE&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;PST&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;         timezone&lt;/span&gt;
&lt;span class="go"&gt;----------------------------&lt;/span&gt;
&lt;span class="go"&gt; 2016-08-12 09:22:31.949271&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which may not &lt;em&gt;look&lt;/em&gt; confusing, but I promise it is because 'PST' is
not actually a timezone! And I can prove it. Postgres keeps a list of
all the timezones it knows in a view called &lt;code&gt;pg_timezone_names&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_timezone_names&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;count&lt;/span&gt;
&lt;span class="go"&gt;-------&lt;/span&gt;
&lt;span class="go"&gt; 1204&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Holy smokes that's a lot of timezones! Over 1200 of these:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_timezone_names&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt; &lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;         name         | abbrev | utc_offset | is_dst&lt;/span&gt;
&lt;span class="go"&gt;----------------------+--------+------------+--------&lt;/span&gt;
&lt;span class="go"&gt; Kwajalein            | MHT    | 12:00:00   | f&lt;/span&gt;
&lt;span class="go"&gt; posix/W-SU           | MSK    | 03:00:00   | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Europe/Vaduz   | CEST   | 02:00:00   | t&lt;/span&gt;
&lt;span class="go"&gt; posix/Pacific/Niue   | NUT    | -11:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; Asia/Ashkhabad       | TMT    | 05:00:00   | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Australia/LHI  | LHST   | 10:30:00   | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Pacific/Tarawa | GILT   | 12:00:00   | f&lt;/span&gt;
&lt;span class="go"&gt; posix/America/Adak   | HDT    | -09:00:00  | t&lt;/span&gt;
&lt;span class="go"&gt; America/Vancouver    | PDT    | -07:00:00  | t&lt;/span&gt;
&lt;span class="go"&gt; Asia/Hovd            | HOVST  | 08:00:00   | t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And not a PST among them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_timezone_names&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PST&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;name | abbrev | utc_offset | is_dst&lt;/span&gt;
&lt;span class="go"&gt;------+--------+------------+--------&lt;/span&gt;
&lt;span class="go"&gt;(0 rows)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But it is this other thing, called an 'abbreviation':&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_timezone_names&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;abbrev&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PST&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;          name          | abbrev | utc_offset | is_dst&lt;/span&gt;
&lt;span class="go"&gt;------------------------+--------+------------+--------&lt;/span&gt;
&lt;span class="go"&gt; Pacific/Pitcairn       | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Pacific/Pitcairn | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; posix/SystemV/PST8     | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; SystemV/PST8           | PST    | -08:00:00  | f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So all of those time zones are the same, and can be abbreviated
'PST'. Note that our friend America/Los_Angeles does not appear here
because it takes daylight savings time into account, whereas PST does
not and is a true UTC-08 offset.&lt;/p&gt;
&lt;p&gt;Most of time this doesn't matter, but it can definitely bite you. For
instance (in fact, the issue that motivated this entire post), I wrote
a bunch of queries that reported on the number of customer membership
renewals each month. These queries all did AT TIME ZONE 'PST'
conversions. Our membership renewal process runs at 11:00pm,
nightly. We're setting up a new BI tool, and I set the session time
zone to Pacific/Los_Angeles, not thinking too hard about it. Well,
guess what? For seven months out of the year, that time zone and PST
differ by an hour. And 11pm is within 1 hour of midnight. So all of
these renewals were suddenly and inexplicably shifted forward by one
day.&lt;/p&gt;
&lt;h2&gt;Recap&lt;/h2&gt;
&lt;p&gt;That's pretty much it! To recap:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use only TIMESTAMP WITH TIME ZONE fields.&lt;/li&gt;
&lt;li&gt;Remember that Postgres doesn't store time zones; it just normalizes
   tz-aware timestamps to UTC.&lt;/li&gt;
&lt;li&gt;Know what time zone your session is using. It might be set deep in
   your application code, or in a config file, or in an env var. For
   your own sanity, make sure all connections to a given database use
   the same session time zone.&lt;/li&gt;
&lt;li&gt;Rely on your session's time zone when possible, rather than using
   AT TIME ZONE operators in your queries.&lt;/li&gt;
&lt;li&gt;Don't ever use AT TIME ZONE transformations on &lt;em&gt;date&lt;/em&gt; fields. Only
   on &lt;em&gt;datetime&lt;/em&gt; fields, if at all.&lt;/li&gt;
&lt;li&gt;Be aware that offsets and time zones are not the same. Most of the
   time it doesn't matter, until it suddenly does.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you want to know even more, I do actually recommend
&lt;a href="https://www.postgresql.org/docs/9.1/static/datatype-datetime.html#DATATYPE-TIMEZONES"&gt;section 8.5.3&lt;/a&gt;
of the PostgreSQL documentation. As far as database server technical
docs goes, it's pretty entertaining. And if that isn't a strong enough
endorsement, then maybe I can tempt you with some gleaned trivia: One
reason PostgreSQL does not recommend using simple TIME WITH TIME ZONE
(e.g. a time with no date component) is because Moscow's MSK offset
means UTC+3 in certain years, and UTC+4 in other years.&lt;/p&gt;
&lt;p&gt;Oof.&lt;/p&gt;
&lt;h2&gt;A Final Mystery&lt;/h2&gt;
&lt;p&gt;I really do feel like I understand how Postgres handles timezones, and
it's all pretty sensible once you know how it works. Except for one
lingering mystery. Check this out:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;#=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pg_timezone_names&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;utc_offset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-08:00:00&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;is_dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;          name          | abbrev | utc_offset | is_dst&lt;/span&gt;
&lt;span class="go"&gt;------------------------+--------+------------+--------&lt;/span&gt;
&lt;span class="go"&gt; Etc/GMT+8              | GMT+8  | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; Pacific/Pitcairn       | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Etc/GMT+8        | GMT+8  | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; posix/Pacific/Pitcairn | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; posix/SystemV/PST8     | PST    | -08:00:00  | f&lt;/span&gt;
&lt;span class="go"&gt; SystemV/PST8           | PST    | -08:00:00  | f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Look at those two extra timezones! Look at their names and
abbreviations! They are UTC-08 offsets, but &lt;em&gt;named&lt;/em&gt; GMT+8?? GMT and
UTC &lt;em&gt;are the same thing&lt;/em&gt;! Google could literally not be more
categorical about this fact:&lt;/p&gt;
&lt;p&gt;&lt;img alt="google-says-so" src="http://blog.untrod.com/images/gmt_and_utc.png"&gt;&lt;/p&gt;
&lt;p&gt;But apparently Postgres thinks they are opposites. Or something. If
anyone can explain this in the comments, I'd really appreciate
it. Thanks!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;edit&lt;/strong&gt; Mystery solved! A very careful reading of section 8.5.3 reveals the secret:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Another issue to keep in mind is that in POSIX time zone names,
positive offsets are used for locations west of Greenwich. Everywhere
else, PostgreSQL follows the ISO-8601 convention that positive
timezone offsets are east of Greenwich.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is unfortunate.&lt;/p&gt;
&lt;h3&gt;Footnotes&lt;/h3&gt;
&lt;p&gt;&lt;a name="footnote1"&gt;1&lt;/a&gt;: Don't believe me? The actual Postgres time
zone documentation
&lt;a href="https://www.postgresql.org/docs/9.1/static/datatype-datetime.html#DATATYPE-TIMEZONES"&gt;opens with&lt;/a&gt;
this confidence-inspiring passage:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Time zones, and time-zone conventions, are influenced by political
decisions, not just earth geometry. Time zones around the world
became somewhat standardized during the 1900s, but continue to be
prone to arbitrary changes&lt;/p&gt;
&lt;/blockquote&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Eight Surprises During My First Year as a Father</title><link href="http://blog.untrod.com/2016/07/observations-from-a-year-of-parenting.html" rel="alternate"></link><published>2016-07-30T00:00:00-07:00</published><updated>2016-07-30T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-07-30:/2016/07/observations-from-a-year-of-parenting.html</id><summary type="html">&lt;p&gt;I have a 13-month-old son named Dashiell (pronounced Dash-uhl, and I'm sorry in advance to schoolage Dashiell, whose teachers will all say "Dah-sheel?" on his first day). He's awesome. I mean...I'm not super objective, but even so, he's definitely a good dude. We're going to keep him.&lt;/p&gt;
&lt;p&gt;Most of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I have a 13-month-old son named Dashiell (pronounced Dash-uhl, and I'm sorry in advance to schoolage Dashiell, whose teachers will all say "Dah-sheel?" on his first day). He's awesome. I mean...I'm not super objective, but even so, he's definitely a good dude. We're going to keep him.&lt;/p&gt;
&lt;p&gt;Most of my friends don't yet have kids, so I've been keeping a mental list of things that no one told me, that surprised me in this first year. With one exception, these eight things are all observations, not advice. Back-seat drivers are annoying, but it's nice to have a heads-up if a large bend in the road is coming.&lt;/p&gt;
&lt;h2&gt;1. The amount of stuff the baby owns trends towards the amount of stuff you own.&lt;/h2&gt;
&lt;p&gt;A baby is a tiny human being, but with full, adult-sized equipment needs. As a long-time dog owner, my pre-baby mental model  was: Baby is size of dog; therefore, baby needs similar amount of stuff as dog. This is not correct.&lt;/p&gt;
&lt;p&gt;The amount of stuff for the baby will, over time, trend towards the amount of stuff any other human being owns. After our baby shower, if you looked around our house, it probably appeared that, in utero, he already owned the majority of the household possessions. Books, toys, furniture, bottles, diapers, burp clothes, bouncers, clothes. Get ready for a massive influx of &lt;em&gt;things&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;2. You are not going to answer emails at 3am when the baby is up.&lt;/h2&gt;
&lt;p&gt;Prior to actually having a kid, I thought that during the first few months of life, when the baby is going to be up in the middle of the night, I would use that time productively; working, answering emails, etc. I'd be tired, but I'd at least get some work done. That is a &lt;em&gt;totally insane idea&lt;/em&gt;, but one that I've also heard from my pre-child coworkers.&lt;/p&gt;
&lt;p&gt;If you are up in the middle of the night with a baby, it is because you are feeding the baby, or attempting to get the baby back to sleep, or thinking &lt;em&gt;"why are you screaming at me at 3am?"&lt;/em&gt;. There is no scenario where you are up in the middle of the night, peacefully answering emails while, like, the baby coos on the floor next to you. If you are capable of answering emails, you are capable of going to sleep. You will pick sleep.&lt;/p&gt;
&lt;h2&gt;3. Development and learning are totally different.&lt;/h2&gt;
&lt;p&gt;Though I sort of knew children 'developed', it took a newborn baby to fully appreciate the difference between development and learning. A baby is not a human that doesn't know things. It is a human that was, only months prior, a loose ball of barely differentiated cells. You cannot teach a ball of cells how to interact with the world. Nor can you teach much to a newborn.&lt;/p&gt;
&lt;p&gt;At each stage, there's a true limit to how much a baby (or child, or teenager, or perhaps adult) is able to learn. Babies need development, not knowledge. Newborn babies are literally unable to look at things. They can't focus their eyes. They are helpless. I've now internalized that, though it slows, &lt;em&gt;development continues throughout life&lt;/em&gt;, including into adulthood. The only book I'm aware of that acknowledges this fact is the mind-blowing &lt;a href="https://www.amazon.com/Over-Our-Heads-Mental-Demands/dp/0674445880"&gt;In Over Our Heads&lt;/a&gt; by Robert Kegan. Adults develop too!&lt;/p&gt;
&lt;h2&gt;4. Sleep and schedule is everything.&lt;/h2&gt;
&lt;p&gt;Warning: Advice! Get the book Babywise, &lt;em&gt;and do what it says&lt;/em&gt;. You child will be sleeping through the night at 12 weeks. Babies that sleep are happy babies. Parents that sleep are happy parents. Babies with happy parents are happy, and parents with happy babies are happy. If this confuses you, then perhaps you should be getting more sleep. But none of this virtuous happiness cycle can happen if everyone is tired, and crabby, and arguing about emptying the dishwasher.&lt;/p&gt;
&lt;p&gt;Also, somewhat counterintuitively, setting a good schedule is liberating; if you can predict when your kiddo will be asleep, or hungry, you can plan your day. &lt;a href="https://www.amazon.com/Becoming-Baby-Wise-Giving-Nighttime/dp/1932740139"&gt;Buy Babywise&lt;/a&gt;. Do it. You are welcome.&lt;/p&gt;
&lt;h2&gt;5. Everyone does it differently, and that's normal&lt;/h2&gt;
&lt;p&gt;&lt;a href="http://www.nytimes.com/2015/02/01/opinion/sunday/the-only-baby-book-youll-ever-need.html"&gt;This article&lt;/a&gt; from the New York Times is a great read. It describes the book &lt;a href="https://www.amazon.com/Anthropology-Childhood-Cherubs-Chattel-Changelings/dp/0521887739"&gt;The Anthropology of Childhood&lt;/a&gt; as "the only baby book you'll ever need" because, once you realize that children are reared every which-way, with totally contrary practices, all over the world, and that kids turn out fine regardless, you can chill the heck out and not sweat the details of whether you are "doing it right". Thanks to my handy summary here, you don't even need to read the article. And you &lt;em&gt;definitely&lt;/em&gt; don't need to read the book; it's fairly academic and boring. But sort of fascinating at the same time.&lt;/p&gt;
&lt;h2&gt;6. The maturation process.&lt;/h2&gt;
&lt;p&gt;To some extent at least, I now believe that "maturation" is simply the process of slowly coming to grips with the fact that you are not the center of the universe. Toddlers, for instance, are not mature.&lt;/p&gt;
&lt;h2&gt;7. The first six months are a little tough for dads.&lt;/h2&gt;
&lt;p&gt;It's fascinating to hold your newborn offspring and watch the human development process those first few months. I could stare at this baby forever, etc. But, like, not really. Frankly, it's a litttttttllle bit boring hanging out with an infant. They don't actually do a whole lot. After a few minutes of goo-gooing and tickling I was just...kinda bored. Moms seem to have a bit of a deeper bond those first months. That's OK. It gets a heck of a lot more interesting and fun. My one year old is hilarious and it's a blast hanging out with him.&lt;/p&gt;
&lt;h2&gt;8. You will feel a little weird. Then you won't.&lt;/h2&gt;
&lt;p&gt;This time last year: Dash is 4 weeks old. I'm out on a walk with him. He is in his bassinet. It's 6pm on Saturday. I see a bunch of guys vaguely my age headed to the bar, getting ready to go out on a warm(-ish) summer night in San Francisco. They glance at me pushing a stroller. I feel very lame and suddenly much older.&lt;/p&gt;
&lt;p&gt;Last weekend: Dash is a little over a year old. It's Saturday evening, we're out for a walk. None of the above registers. I still go out with my friends (though somewhat less frequently), and nothing about me having a kid is weird anymore. I feel like a young parent, and my kid is awesome, and I love my wife. We're almost out of whole milk, so I've picked some up, along with some salmon to grill in the back yard. We run into our neighbor, who has an eight month old, and swap a couple of jokes, and promise, for the 10th time, to get together for dinner soon. Life is good and I am happy.&lt;/p&gt;
&lt;h2&gt;So that's it.&lt;/h2&gt;
&lt;p&gt;Basically, just relax. People way less competent that you have raised perfectly reasonable, happy kids. Re-read observation #5 when you get nervous, look forward to awesomely fun times as per point #8, and for the love of god do #4. Good luck and I look forward to seeing our children play against each other in the NBA, but still be civil when mingling backstage before their respective TED talks.&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/dash-peeking.jpg"&gt;
&lt;em&gt;Fig 1. Chubby tiptoes.&lt;/em&gt;&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Zero Downtime Rebrand</title><link href="http://blog.untrod.com/2016/07/zero-downtime-rebrand.html" rel="alternate"></link><published>2016-07-12T00:00:00-07:00</published><updated>2016-07-12T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-07-12:/2016/07/zero-downtime-rebrand.html</id><summary type="html">&lt;p&gt;&lt;a href="https://github.com/noazark/"&gt;Noah Smith&lt;/a&gt; and I gave this presentation at the San Francisco Django meetup in March of 2016, shortly after we rebranded ePantry to Grove Collaborative. Here's how we pulled it off.&lt;/p&gt;


&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-01.png"&gt;
&lt;em&gt;Welcome folks!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-02.png"&gt;
&lt;em&gt;That's us&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-03.png"&gt;
&lt;em&gt;We rebranded on March 8, 2016. Grove is an aspiration brand that speaks to our …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/noazark/"&gt;Noah Smith&lt;/a&gt; and I gave this presentation at the San Francisco Django meetup in March of 2016, shortly after we rebranded ePantry to Grove Collaborative. Here's how we pulled it off.&lt;/p&gt;


&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-01.png"&gt;
&lt;em&gt;Welcome folks!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-02.png"&gt;
&lt;em&gt;That's us&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-03.png"&gt;
&lt;em&gt;We rebranded on March 8, 2016. Grove is an aspiration brand that speaks to our mission and to our customers. ePantry is a practical brand based in the how's and what's instead of the 'why'. We're leaving ePantry behind. Concretelly, this means a redesign, renaming everything to Grove, and moving from epantry.com to grove.co (we couldn't get the .com).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-04.png"&gt;
&lt;em&gt;Pretty standard stuff. The bulk of the company is run via a Django monolith webservice backend, and a single-page backbone app on the frontend. We also have an iOS app that consumes the same Django webservices.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-05.png"&gt;
&lt;em&gt;Here's what the site used to look like under the ePantry brand.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-06.png"&gt;
&lt;em&gt;And here's the Grove brand.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-07.png"&gt;
&lt;em&gt;We did this really, really fast. Note that we got the new design (colors, fonts, logo) only 2 weeks before launch. Ah, startups.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-08.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-09.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-10.png"&gt;
&lt;em&gt;Cloudflare is awesome. It allowed us to have both epantry.com and grove.co point to the same Heroku app (which can only have a single SSL cert).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-11.png"&gt;
&lt;em&gt;Here's how the SSL config works. This is probably a 90% solution, in that if you inspect the certs, you'll see this somewhat janky cloudflaressl cert at the end of the chain, instead of a grove.co cert. We still get the green lock in all browsers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-12.png"&gt;
&lt;em&gt;Foreshadowing! We'll be redirecting everyone from epantry.com to the new grove.co domain. Can you guess what bad thing is going to happen?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-13.png"&gt;
&lt;em&gt;This is bad. We are a subscription commerce site. The last thing you want to do is log out every single one of your users. That is not a way to help retention during the changeover. Not to mention our emails are going to be coming from a different domain, with less sender reputation, and customers may not recognize them. Password managers also won't be aware of the new domain.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-14.png"&gt;
&lt;em&gt;This is all Noah! He came up with a great way to keep folks logged in (securely) across domains.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-15.png"&gt;
&lt;em&gt;It basically operates like an OAuth flow; an anonymous user comes to grove.co, we redirect them to epantry.com, detect that there is an active session, then (on the server) literally copy their session data in the database from the epantry.com session into the new grove.co session, and then finally redirect them to their final destination. The code for this is &lt;a href="https://www.github.com/groveco/rebrand"&gt;available on github&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-16.png"&gt;
&lt;em&gt;In general (and in addition to the session-exchange middleware) we built middleware to redirect all traffic from the old domain to the new one. However there is some traffic we discovered we should not redirect, and instead wanted served by the requested domain. Spiders can get redirected, but not session-exchanged because they do not reliably store cookies. Webhooks get confused by redirects as well, so we just continue serving any webhooks from the original domain. Eventually, we managed to move all webhooks over to the new domain, but there were enough moving pieces without doing that prior to launch.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-17.png"&gt;
&lt;em&gt;This caught us by surprise. With all of the redirects in Cloudflare to enforce https and www, then all of the session exchange middleware, the redirects were adding up fast! Combine that with the crazy redirects that some advertising vendors use to track theie links, and you could be pushing the browser limit of 20 redirects (10 in old versions of IE!) pretty quickly. We didn't end up hitting it in practice, but something to watch out for. Not to mention the performance hit.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-18.png"&gt;
&lt;em&gt;Once all of this was set up, we started testing in the wild in production right away. We added a staff check to the middleware so all employees could kick the tires for a couple of weeks on the new domain and try to flush out any issues. Note that the overall redirect is controlled by a Django setting, which maps to an environmental variable. The idea being that when it came time to launch, we could just flip the switch.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-19.png"&gt;
&lt;em&gt;We didn't want staff copy-pasting URLs to the new domain accidentally, so we created template tags that make folks very aware, if they were on the new domain prior to launch. Also of note, the Sites framework was totally useless during this entire process since it is designed for totally parallel sites, not a single site living on two domains.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-20.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-21.png"&gt;
&lt;em&gt;As mentioned on the timeline, we didn't get the design until late in the game. But that didn't mean we couldn't start work! There was a ron of prep to get ready so that, regardless of how the new design turned out, we could get it implemented fast.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-22.png"&gt;
&lt;em&gt;The first thing was to clean up the colors. This bash command will find all hex colors in our sass stylesheets so we can see how far we've drifted from a common color palette.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-23.png"&gt;
&lt;em&gt;Same thing for font styles. We use Foundation and all typography should be coming from a small handful of framework classes and styling that we override with sass settings. In theory we should just not have any matches to this expression if we're doing it right (hint: we were not).&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-24.png"&gt;
&lt;em&gt;Ugh.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-25.png"&gt;
&lt;em&gt;It's totally amazing how out of control this stuff can get over a few years of rapid development. This was a great opportunity to go back and clean it up. Look at that reduction in colors! And they are all in well-names sass variables for easy resuse. One thing we would strongly encourage for anyone embarking on a project like this is to force their designers to be really consistent with the colors in their mocups. It's easy to say "here's the new palette" but then have subtle differences in different mocked screenshots. That led to a lot of confusion on a short timeline.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-26.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-27.png"&gt;
&lt;em&gt;Holy moly is font licensing more complex than I anticipated. I'm not really joking when I say "I'm pretty sure you are allowed to read thi." I'm not entirely sure the font license for that title font ("Archer") covers use in a presentation like this. And all of these screenshots are real, from a SINGLE font company, that I had to contend with to purchase and use ONE font. Nuts. If we had known this earlier in the process, I would have made a much stronger recommendation that we just stick to what's available in Google Fonts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-28.png"&gt;
&lt;em&gt;Not to mention all of the additional infrastructure involved in using fonts from multiple sources. Thanks Adobe Type Cloud -- I didn't USED to have to dedicate IT resources to fonts, when we were using Google, but I sure as hell do now.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-29.png"&gt;
&lt;em&gt;Go time!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-30.png"&gt;
&lt;em&gt;Look at that Git diff! It's bleeding red! That's what you get from cleaning up all that CSS. We deployed all of the server side code weeks in advance of launch. The actual deploy just consisted of a bunch of static assets deploying. We actually did a test deploy at midnight before launch to verify things worked, and then rolled it back. There were probably a handful of very confused night-owl customers. The once piece you can't take back is redirects. Browsers cache them aggressively so if you do have to roll something back, anyone who hit the redirects will still get pointed to your new domain. Redirects are forever.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-31.png"&gt;
&lt;em&gt;Here's the real checklist I used for go-live. Each department had their own. It stayed fairly organized thanks to Emacs and Org Mode. A bunch of this stuff could have, in theory, been scripted. But it was easy enough to write out really explicit steps and just do them by hand at launch time. Not everything has to be totally automated.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-32.png"&gt;
&lt;em&gt;These charts are from Librato. We piped a bunch of custom metrics in, including number of requests to each domain, and watched for anomalous HTTP codes. You can see the cutover happen in the top chart, around 6am as traffic moves from epantry.com (green) to the new domain (blue). Smooth sailing!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-33.png"&gt;
&lt;em&gt;Customers getting redirected to the new domain for the first time got a full-page model popover introducing them to the new brand. We used cookies to remember if they had seen it and/or dismissed it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/rebrand-slides/20160330-Rebrand-Django-Meetup-34.png"&gt;
&lt;em&gt;The orange line shows our recurring purchases for the month. The green line is our target budget number. If the rebrand went poorly (if customers either didn't like it, or were confused) then the canary in the coal mine would be a drop off in repurchase rate. But we saw no such thing and stayed precisely on track. Success!&lt;/em&gt;&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>The First Thing I Tell New Engineers</title><link href="http://blog.untrod.com/2016/06/the-first-thing-i-tell-new-engineers.html" rel="alternate"></link><published>2016-06-30T00:00:00-07:00</published><updated>2016-06-30T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-06-30:/2016/06/the-first-thing-i-tell-new-engineers.html</id><summary type="html">&lt;p&gt;The first thing I tell junior programmers is: &lt;strong&gt;keep going&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you've been programming a while, you know that feeling of
encountering a new project, with a new stack, in a new language. The
feeling is: &lt;em&gt;OK.&lt;/em&gt; And you roll up your sleeves and get to
work.&lt;/p&gt;


&lt;p&gt;But do you …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The first thing I tell junior programmers is: &lt;strong&gt;keep going&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you've been programming a while, you know that feeling of
encountering a new project, with a new stack, in a new language. The
feeling is: &lt;em&gt;OK.&lt;/em&gt; And you roll up your sleeves and get to
work.&lt;/p&gt;


&lt;p&gt;But do you remember the first time you were in a professional setting,
in front of a new project with years of engineer-hours invested in it?
You are staring at a foreign IDE for the first time, and a senior
engineer has told you to fix a bug. You have 20 new technologies to
learn, all at once, and you don't have a solid conceptual model for
how they fit together. The syntax might as well be a Dwarf Fortress
game, and you're 10 steps away from even being able to figure out what
to Google.&lt;/p&gt;
&lt;p&gt;It's overwhelming.&lt;/p&gt;
&lt;p&gt;And, you know what? I still find it overwhelming. I recently started
learning ClojureScript. The recommended first step is basically,
"spend 2 years learning emacs". Then learn the Java ecosystem. Then
learn Clojure. Then learn ClojureScript. Yikes.&lt;/p&gt;
&lt;p&gt;But the difference between wet-behind-the-ears, just-graduated Chris
Clark, and the me of today, is that I've had this feeling dozens, or
hundreds, of times before. I just barrel through. I know that all of
this &lt;em&gt;stuff&lt;/em&gt; was built by other engineers. Some of them were smarter
than me, some of them weren't. It doesn't matter; I have complete
confidence that if I perservere, I will figure it out. No doubts at
all.&lt;/p&gt;
&lt;p&gt;So that's what I tell new engineers: When you worry you might &lt;em&gt;not&lt;/em&gt;
figure it out? That you weren't cut out for this? I've been there, and
then been there again. But you'll figure it out, I promise. Just keep
going.&lt;/p&gt;
&lt;h4&gt;The Second Thing I Tell New Engineers&lt;/h4&gt;
&lt;p&gt;The second thing I tell new engineers (specifically, new grads) is:&lt;/p&gt;
&lt;p&gt;In college, a tough professor might give you a huge
assignment that might take you 20 hours. What a jerk! In the
full-time-work world, that's a project that's done by
Thursday.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Software at Companies that Don't Sell Software</title><link href="http://blog.untrod.com/2016/06/software-at-companies-that-dont-sell-software.html" rel="alternate"></link><published>2016-06-29T00:00:00-07:00</published><updated>2016-06-29T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-06-29:/2016/06/software-at-companies-that-dont-sell-software.html</id><summary type="html">&lt;p&gt;If you're a software professional, there are two types of
companies you might work for; those whose products are fundamentally
software (they make money directly from technology), and those that
make their money some other way, but rely on software. Let's call the
first Type A, and the second Type …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you're a software professional, there are two types of
companies you might work for; those whose products are fundamentally
software (they make money directly from technology), and those that
make their money some other way, but rely on software. Let's call the
first Type A, and the second Type B&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;Type A Companies&lt;/h2&gt;
&lt;p&gt;These folks sell software, or otherwise directly monetize their
technology.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Microsoft&lt;/li&gt;
&lt;li&gt;Twilio&lt;/li&gt;
&lt;li&gt;Dropbox&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Type B Companies&lt;/h2&gt;
&lt;p&gt;These guys are enabled by technology, but don't make money from it
directly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FedEx (fundamentally a logistics company)&lt;/li&gt;
&lt;li&gt;WalMart (big website, complex internal systems...but a retailer)&lt;/li&gt;
&lt;li&gt;Wells Fargo (and other financial institutions)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Biased&lt;/h2&gt;
&lt;p&gt;Early in my career, one of the things I looked for on resumes was
experience at a Type A company. I believed that the best engineers
would gravitate away from Type B companies. I thought Type A employees
were more likely to be at the top of the talent heap, and more likely
to be current with the tools and practices of the trade.&lt;/p&gt;
&lt;p&gt;But in the intervening years, two things have become apparent:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Where there is a big, successful business, there are great
   people&lt;sup&gt;&lt;a href="#footnote2"&gt;2&lt;/a&gt;&lt;/sup&gt;, and great systems&lt;sup&gt;&lt;a href="#footnote3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/li&gt;
&lt;li&gt;Engineers with Type B experience are more likely to play well with
   others.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I can't stand engineering groups with superior attitudes, that aren't
supportive of the rest of the organization. A little competition
between departments is healthy, but if we're all building bicycles for
the mind, then fundamentally our job is to augment others. It's a
service job -- we build tools. A humble, cooperative attitude is
invaluable.&lt;/p&gt;
&lt;p&gt;There are a few reasons I think Type B companies do a better job
fostering this attitude.&lt;/p&gt;
&lt;h2&gt;Software Engineer at a Type A company&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Your job is to build awesome technology that customers will pay for.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You are close to the money&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It's straightforward to be a technologist at a Type A company. The
software you create is very closely tied to revenue, which is
ultimately what drives business. The closer you are to the money, the
closer you are to the beating heart of a company.&lt;/p&gt;
&lt;p&gt;On the income statement, engineering &amp;amp; R&amp;amp;D are a cost
centers&lt;sup&gt;&lt;a href="#footnote4"&gt;4&lt;/a&gt;&lt;/sup&gt;, but they are also the golden
goose that made the business possible in the first place.&lt;/p&gt;
&lt;p&gt;In the words of one engineer I worked with "If it doesn't come out of
these fingers, it doesn't happen."&lt;/p&gt;
&lt;h2&gt;Software Engineer at a Type B company&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Your job is to support the business goals of the organization.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You are further from the money&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In a Type B company, you're a true cost center, and perhaps viewed
with a bit of skepticism. Is all this software really necessary?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your work is more likely to be internal, rather than
customer-facing.&lt;/li&gt;
&lt;li&gt;It's more likely to be an auxiliary function, as
opposed to a market differentiator&lt;ul&gt;
&lt;li&gt;Even at tech-heavy Type B companies this is
  true&lt;sup&gt;[3](#footnote3&lt;/sup&gt;; Amazon's differentiator is not
  the quality of their website. It's their selection, pricing
  power, and market position.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;When it's not a differentiator, every project is subject to a
  build-or-buy decision.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Money is the central pillar of business, and the further from it you
are, the less influence you have.&lt;/p&gt;
&lt;h2&gt;Closer to the Customer&lt;/h2&gt;
&lt;p&gt;But working for a Type B company can also be a heck of a lot of
fun. Your customers are often the folks sitting down the hall from
you. Product feedback is instant. You might get a literal pat on the
back for a job well done. Your users have less variance in terms of
training and environment, and everyone shares a lot of tribal
knowledge.&lt;/p&gt;
&lt;p&gt;Good engineering is 20% building product right, and 80% building right
product. With the tight feedback loop you get developing internally,
you're much more likely to get it right. And with a forgiving customer
base, often running on the same infrastructure, you can iterate
quickly.&lt;/p&gt;
&lt;h2&gt;A Healthier Mind-set&lt;/h2&gt;
&lt;p&gt;If I never overhear the following at my company, we're doing good
work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"I can't get cycles from the engineering group."&lt;/li&gt;
&lt;li&gt;"Engineering doesn't think it's a priority."&lt;/li&gt;
&lt;li&gt;"I'm nervous to ask because I don't want to distract anyone on the
  engineering team."&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether engineering is at the top or the bottom of the cultural
hierarchy, technology exists to improve the lives of its
users. Engineering is a functional group like marketing, or sales, or
operations. These teams need to work alongside each other as
peers. The cultures in Type B companies are more likely to foster that
mind set.&lt;/p&gt;
&lt;h3&gt;Notes&lt;/h3&gt;
&lt;p&gt;&lt;a name="footnote1"&gt;1&lt;/a&gt;: This doesn't strictly apply to only
software companies. Boeing, for instance, clearly makes money directly
from their technology products...which are not strictly software
products. I would describe them more as giant flying tubes of metal.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote2"&gt;2&lt;/a&gt;: I do still believe that bad engineers are
more likely to be hiding out at Type B companies, but that has no
bearing if you are trying to recruit top performers. You'll spot them
right away. The difference between MIT and Virginia Polytech is not at the
top of the class; both have some of the world's best. But at the
bottom of the class, there's more room to hide in a larger, less
focused organization.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote3"&gt;3&lt;/a&gt;: Popular, influential open-source projects
came out of Type B companies all the time. The New York Times alone
produces incredible open source projects, from D3 to their Objective-C
Style Guide, to Gizmo. And don't forget that Django was created by the
Lawrence Journal-World newspaper.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote4"&gt;4&lt;/a&gt;: Excluding Amazon Web Services, of
course. I'm talking about Amazon the retailer.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Trello vs. Asana</title><link href="http://blog.untrod.com/2016/06/trello-vs-asana.html" rel="alternate"></link><published>2016-06-23T00:00:00-07:00</published><updated>2016-06-23T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-06-23:/2016/06/trello-vs-asana.html</id><summary type="html">&lt;p&gt;In two totally different companies over the past 4 years, across
different personalities, teams, and processes, I've seen Asana
rejected no fewer than a dozen times in favor of Trello, in spite of
multiple executive mandates to use Asana.&lt;/p&gt;


&lt;p&gt;In both companies, management (of which I am a part) made …&lt;/p&gt;</summary><content type="html">&lt;p&gt;In two totally different companies over the past 4 years, across
different personalities, teams, and processes, I've seen Asana
rejected no fewer than a dozen times in favor of Trello, in spite of
multiple executive mandates to use Asana.&lt;/p&gt;


&lt;p&gt;In both companies, management (of which I am a part) made a hard push
to use Asana. Everyone gave it a shot, and then somehow ended up back
on Trello.&lt;/p&gt;
&lt;p&gt;I gave up on foisting Asana (or any other task management tool) on my
teams in 2013, but I can't quite convince everyone else that it's a
futile practice. I think all managers eventually learn this lesson
though; it's hard to make teams use tools that don't fit their hands.&lt;/p&gt;
&lt;p&gt;But that's something to explore another time. Here, I just want to
think about why &lt;em&gt;specifically&lt;/em&gt; Asana keeps getting surreptitiously
replaced with Trello as soon as management stops paying attention.&lt;/p&gt;
&lt;p&gt;Both tools aim to do about the same thing, and have comparable feature
sets. They can both operate as Trojan horses; if a department wants to
use it, they just start using it; there's no installation or setup
period. But management seems to prefer one, and actual workers seem to
prefer the other.&lt;/p&gt;
&lt;p&gt;Here's what I think is going on:&lt;/p&gt;
&lt;h1&gt;Asana&lt;/h1&gt;
&lt;p&gt;There are projects, tags, sections, and subtasks. And they are all
kinda-sorta similar and you can pivot tasks along any of those
dimensions. On top of all of that, there are roadmaps, and
pipelines. You can sort and filter and drill down and through and
around on everything.&lt;/p&gt;
&lt;p&gt;The result is great for managers. It's easy to see who is working on
what, and how tasks are connected, and where things stand.&lt;/p&gt;
&lt;p&gt;The result is terrible for employees. What am I supposed to be working
on? Where are all my tasks? What is the top priority? Every view gives
me a slightly different answer to these questions. I have a general
sense that things are slipping through the cracks.&lt;/p&gt;
&lt;h1&gt;Trello&lt;/h1&gt;
&lt;p&gt;There are boards. Boards have lists. Lists have cards. Cards have a
bunch of stuff. Pretty much the only way to look at anything is via a
board, full of lists, full of cards.&lt;/p&gt;
&lt;p&gt;This is extremely annoying if you are trying to figure out, for
instance, "what is Chris working on right now?", or even "What did
Chris do last week?". Because things are spread across
boards&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt;, and because Trello cards don't have
any status, there's just no real way to do any kind of reporting.&lt;/p&gt;
&lt;p&gt;On the other hand, one of our most used Trello boards is called
"Jordan can you please..." and just contains a ton of projects and
tasks (list and cards) Jordan is working on. This works extremely
well, and it has a funny name, which is always a bonus. If you want
Jordan's help with something, guess where you put the card? And if you
want to see the status of your card, guess where you look?&lt;/p&gt;
&lt;h1&gt;In Conclusion&lt;/h1&gt;
&lt;p&gt;I've had very few meetings to discuss across teams how we might
organize our Trello boards. There aren't a ton of options, and who
cares anyway, since there's no benefit to standardization since there's no reporting.&lt;/p&gt;
&lt;p&gt;I've had a lot of "How should we all use Asana?" meetings.&lt;/p&gt;
&lt;p&gt;Trello makes it easy to manage my own work, and that of my immediate
colleagues. Asana makes it easy to manage other people's work. No
surprise that managers like Asana, but every time they look at the
screens around the office, everyone is using Trello.&lt;/p&gt;
&lt;h3&gt;Notes&lt;/h3&gt;
&lt;p&gt;&lt;a name="footnote1"&gt;1&lt;/a&gt;: Not to mention I still cannot for the life
of me figure out how organizations or teams really work in Trello.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Pushing Bits with Python</title><link href="http://blog.untrod.com/2016/06/pushing-bits-with-python.html" rel="alternate"></link><published>2016-06-17T00:00:00-07:00</published><updated>2016-06-17T00:00:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2016-06-17:/2016/06/pushing-bits-with-python.html</id><summary type="html">&lt;p&gt;When was the last time you needed to directly manipulate a bunch of
binary data? Most engineers run
across it only in the context of bit flags and masks in C++ or Java.&lt;/p&gt;
&lt;p&gt;It's a little rarer to muck about with direct binary file
data. It's nonetheless quite a bit …&lt;/p&gt;</summary><content type="html">&lt;p&gt;When was the last time you needed to directly manipulate a bunch of
binary data? Most engineers run
across it only in the context of bit flags and masks in C++ or Java.&lt;/p&gt;
&lt;p&gt;It's a little rarer to muck about with direct binary file
data. It's nonetheless quite a bit of fun, and a good party
trick&lt;sup&gt;&lt;a href="#footnote1"&gt;1&lt;/a&gt;&lt;/sup&gt;. And, how are you going to solve the
next &lt;a href="https://en.wikipedia.org/wiki/Cicada_3301"&gt;Cicada 3301&lt;/a&gt; puzzle
without being able to XOR together a bunch of Tor Onion addresses with
&lt;a href="http://uncovering-cicada.wikia.com/wiki/XOR_all_the_things"&gt;mysterious audio signals&lt;/a&gt;&lt;sup&gt;&lt;a href="#footnote2"&gt;2&lt;/a&gt;&lt;/sup&gt;?&lt;/p&gt;
&lt;p&gt;If you want to learn, then good news -- Python has you covered! The
awesome &lt;a href="http://pythonhosted.org/bitstring/"&gt;bitstring&lt;/a&gt; module makes
it simple to take raw data in whatever form you have it (bits, hex,
octal, a file stream) and inspect or manipulate it. Be sure to &lt;code&gt;pip
install bitstring&lt;/code&gt; to follow along. Away we go!&lt;/p&gt;
&lt;h2&gt;Create a BitArray from scratch&lt;/h2&gt;
&lt;p&gt;To start, let's use bitstring to create a raw 12-bit binary object,
and initialize it to 256. Then we'll take a look at the object in a
bunch of different raw formats.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;bitstring&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;

&lt;span class="n"&gt;twofiftysix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BitArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that we specified the length of the BitArray so that we have
enough bits to convert to Hex and Octal. If we just took 256 as
100000000, then we can't display a corresponding Hex string because
each Hex character represents 4 bits, and 256 in binary is only 9 bits
long. By specifying a length of 12, our BitArray is padded with 3
leading 0s, and we can convert cleanly into Hex and Octal.&lt;/p&gt;
&lt;p&gt;Show me the money!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Int: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;, Binary: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;, Hex: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;, Oct: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;twofiftysix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;twofiftysix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;twofiftysix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;twofiftysix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Int: 256, Binary: 000100000000, Hex: 100, Oct: 0400&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Manipulation&lt;/h2&gt;
&lt;p&gt;We can manipulate the bits directly as well. Here we'll create a
couple of BitArrays, and XOR them together. Bitstring overrides the ^
operator to perform an XOR.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;bits1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BitArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2ba49fe&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bits2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BitArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;f55e513&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;bits1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;bits2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;                    XOR&amp;#39;ed =&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits1&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;bits2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;And in hex:&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits1&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;bits2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; = &amp;#39;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits1&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;bits2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;#39;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Results:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;0010101110100100100111111110&lt;/span&gt;
&lt;span class="mf"&gt;1111010101011110010100010011&lt;/span&gt;
                    &lt;span class="n"&gt;XOR&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;ed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="mf"&gt;1101111011111010110011101101&lt;/span&gt;

&lt;span class="n"&gt;In&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="mf"&gt;1101111011111010110011101101&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="kd"&gt;def&lt;/span&gt;&lt;span class="n"&gt;aced&lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Did you know 'defaced' is the longest english word you can spell with
just ABCDEF!  Another awesome fact for your cocktail party banter.&lt;/p&gt;
&lt;h2&gt;File type detection&lt;/h2&gt;
&lt;p&gt;Here's something a little more practical. Let's say we have
&lt;a href="http://blog.untrod.com/files/mystery-images.zip"&gt;4 images&lt;/a&gt;, with no file
extensions, called mystery1, mystery2, mystery3, and mystery4. Some
quick Googling will tell us the leading bits of some common image
types. We can load up the images, inspect the first bits, and figure
out the file type.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;bmp&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;424d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Converted to ASCII, this is &amp;#39;BM&amp;#39;&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;gif&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;474946&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Converted to ASCII, this is &amp;#39;GIF&amp;#39;&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;jpeg&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ffd8ff&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;png&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;89504e470d0a1a0a&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mystery1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mystery2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mystery3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mystery4&amp;#39;&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;img_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; is &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; (first bits were &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Could not identify &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;. First bits: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Problem solved!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mystery1 is jpeg (first bits were ffd8ffe1)
mystery2 is gif (first bits were 47494638)
mystery3 is png (first bits were 89504e47)
mystery4 is bmp (first bits were 424d0072)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Converting to &amp;amp; from ASCII&lt;/h2&gt;
&lt;p&gt;One last trick; using the built-in binascii library, we can convert ascii to bits and back.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# The built-in binascii library can convert binary to bits&lt;/span&gt;
&lt;span class="nb"&gt;hex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binascii&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexlify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;foobar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BitArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We get:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;011001100110111101101111011000100110000101110010&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...which we can then convert back to an ASCII string:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;binascii&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unhexlify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;foobar&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Notes&lt;/h3&gt;
&lt;p&gt;&lt;a name="footnote1"&gt;1&lt;/a&gt;: If this does not impress, you are obviously
going to the wrong sorts of parties.&lt;/p&gt;
&lt;p&gt;&lt;a name="footnote2"&gt;2&lt;/a&gt;: For what it's worth, this is literally the
reason the learned how to do this in the first place. Is that
embarassing, or cool? I guess it depends what kind of party you're at.&lt;/p&gt;</content><category term="code &amp; tutorials"></category></entry><entry><title>A Simple Content-Based Recommendation Engine in Python</title><link href="http://blog.untrod.com/2016/06/simple-similar-products-recommendation-engine-in-python.html" rel="alternate"></link><published>2016-06-09T05:15:00-07:00</published><updated>2016-06-09T05:15:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2016-06-09:/2016/06/simple-similar-products-recommendation-engine-in-python.html</id><summary type="html">&lt;p&gt;Let's pretend we need to build a recommendation engine for an
eCommerce web site.&lt;/p&gt;
&lt;p&gt;There are basically two approaches you can take:
content-based and collaborative-filtering. We'll look at some pros and
cons of each approach, and then we'll dig into a
&lt;a href="https://github.com/groveco/content-engine/blob/master/engines.py"&gt;simple implementation&lt;/a&gt;
(ready for deployment on Heroku!) of a …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Let's pretend we need to build a recommendation engine for an
eCommerce web site.&lt;/p&gt;
&lt;p&gt;There are basically two approaches you can take:
content-based and collaborative-filtering. We'll look at some pros and
cons of each approach, and then we'll dig into a
&lt;a href="https://github.com/groveco/content-engine/blob/master/engines.py"&gt;simple implementation&lt;/a&gt;
(ready for deployment on Heroku!) of a content-based engine.&lt;/p&gt;
&lt;p&gt;For a sneak peak at the results of this approach, take a look at how
we use a nearly-identical recommendation engine
&lt;a href="https://www.grove.co/catalog/product/cellulose-sponge/?v=802"&gt;in production at Grove&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;How Content-Based Recommenders Works&lt;/h2&gt;
&lt;p&gt;Content-based systems are the ones that your friends and colleagues
all assume you are building; using actual item properties like
description, title, price, etc. If you had never thought about
recommendation systems before, and someone put a gun to your head,
Swordfish-style, and forced you to describe one out loud in 30
seconds, you would probably describe a content-based system. "Uhh,
uhh, I'd like, show a bunch of products from the same manufacturer
that have a similar description."&lt;/p&gt;
&lt;p&gt;You're using the actual attributes of the item itself to recommend
similar products. This makes a ton of sense, as it's how we actually
shop in the real world. We go into the Toaster-Oven aisle and look at
all the toaster ovens, which are probably physically arranged on the
shelf according to brand, or price, or ability to also cook a full
turkey in under 30 minutes.&lt;/p&gt;
&lt;h2&gt;Where Content-Based Falls Short&lt;/h2&gt;
&lt;p&gt;On most eCommerce sites it's already easy enough for folks to
&lt;a href="http://www.target.com/c/toaster-ovens-kitchen-appliances/-/N-5xtri"&gt;browse the toaster oven category&lt;/a&gt;. What
we really want is a recommendation system that drives incremental
sales (e.g. sales that would not have happened otherwise). If a
customer is looking at the product details page for Harry Potter and
the Chamber of Secrets, and your recommender shows Prisoner of
Azkaban, and the customer buys it, the data scientists back at Random
House HQ should &lt;em&gt;not&lt;/em&gt; be high-fiving. It's a safe bet that that
customer already knew there were more than two books in the series and
would have bought Prisoner of Azkaban anyway. It was &lt;em&gt;not&lt;/em&gt; an
incremental sale.&lt;/p&gt;
&lt;h2&gt;How Collaborative Filtering Recommenders Work&lt;/h2&gt;
&lt;p&gt;We need another approach. Enter Collaborative Filtering, or CF. The
big idea behind CF is also pretty intuitive; the product someone is
most likely to buy, is the product that a bunch of people like you
also bought. Sure, this can lead to the Harry Potter situation, but
it's much better at making recommendations from further afield, from
deeper in the product catalog. It's more robust against problems like
typos ("Harry Pooter" still gets recommended), and when measured in
the real world in terms of generating incremental sales, generally
beats the pants off pure content-based systems.&lt;/p&gt;
&lt;p&gt;While the big idea behind CF is intuitive, there is one aspect that
you will definitely have to explain to colleagues many, many
times. Pure CF systems have &lt;em&gt;no knowledge whatsoever&lt;/em&gt; about the
products they are recommending! To the system, it's just a giant grid
of product IDs and user IDs, representing who bought what. It's deeply
counterintuitive that CF algorithms often see no measurable
performance improvements when they are hybridized with content-based
systems. Surely knowing &lt;em&gt;something&lt;/em&gt; about the items you are
recommending must help a &lt;em&gt;little&lt;/em&gt;, right?&lt;/p&gt;
&lt;p&gt;Nope.&lt;/p&gt;
&lt;p&gt;In most cases, essentially 100% of the 'signal' is retrievable from a
simple matrix of who bought what.&lt;/p&gt;
&lt;p&gt;So why on earth would you use content-based approaches?&lt;/p&gt;
&lt;h2&gt;When Content-Based Approaches Make Sense&lt;/h2&gt;
&lt;p&gt;But sometimes CF isn't a viable option; let's say we want to make
recommendations to a customer viewing a product details page, who just
got there from a Google SERP link. We don't know anything about this
customer, so we can't build a matrix of purchases. But we &lt;em&gt;can&lt;/em&gt; use a
content-based system to recommend similar products. In that sense,
content-based recommenders can solve the "cold-start" problem that CF
systems have.&lt;/p&gt;
&lt;p&gt;They can also provide a measure of automated curation when you have a
strong indication of buying intent for a particular product (like when
a lead comes from a Google search for a term related to that
product). If you're interested in the
&lt;a href="http://store.nike.com/us/en_us/pd/pro-hypercool-fitted-shirt/pid-10862654/pgid-11296413"&gt;Nike Pro Hypercool Fitted Men's Compression Shirt&lt;/a&gt;,
you might also love the
&lt;a href="http://store.nike.com/us/en_us/pd/pro-hypercool-print-3-4-tights/pid-10862709/pgid-11296417"&gt;Nike Pro Hypercool Printed Men's Tights&lt;/a&gt;. A
content-based engine rocks at picking up related products like this,
without a bunch of manual curation (those products don't appear
together in the 'pants' category, nor in 'shirts').&lt;/p&gt;
&lt;h2&gt;Let's Build it with TF-IDF.&lt;/h2&gt;
&lt;p&gt;Like many algorithms, we can use a bunch of off-the-shelf libraries to
make life pretty easy. As I walk through the approach, bear in mind
that the entire implementation is going to ultimately be
&lt;a href="https://github.com/groveco/content-engine/blob/master/engines.py#L50"&gt;fewer than 10 lines of Python&lt;/a&gt;. But
before we get to the big reveal and look at the code, let's talk
through the approach.&lt;/p&gt;
&lt;p&gt;I've provided a sample dataset of outdoor clothing and products
from Patagonia. The data looks like this, and you can view the whole
thing (~550kb)
&lt;a href="https://github.com/groveco/content-engine/blob/master/sample-data.csv"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;id&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;description&lt;/span&gt;                                                                 &lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|----|-----------------------------------------------------------------------------|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Active&lt;/span&gt; &lt;span class="nv"&gt;classic&lt;/span&gt; &lt;span class="nv"&gt;boxers&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;There&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;s a reason why our boxers are a cult favori...|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Active&lt;/span&gt; &lt;span class="nv"&gt;sport&lt;/span&gt; &lt;span class="nv"&gt;boxer&lt;/span&gt; &lt;span class="nv"&gt;briefs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;Skinning&lt;/span&gt; &lt;span class="nv"&gt;up&lt;/span&gt; &lt;span class="nv"&gt;Glory&lt;/span&gt; &lt;span class="nv"&gt;requires&lt;/span&gt; &lt;span class="nv"&gt;enough&lt;/span&gt; &lt;span class="nv"&gt;movement&lt;/span&gt; &lt;span class="nv"&gt;wi&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Active&lt;/span&gt; &lt;span class="nv"&gt;sport&lt;/span&gt; &lt;span class="nv"&gt;briefs&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;These&lt;/span&gt; &lt;span class="nv"&gt;superbreathable&lt;/span&gt; &lt;span class="nv"&gt;no&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;fly&lt;/span&gt; &lt;span class="nv"&gt;briefs&lt;/span&gt; &lt;span class="nv"&gt;are&lt;/span&gt; &lt;span class="nv"&gt;the&lt;/span&gt; &lt;span class="nv"&gt;minimal&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Alpine&lt;/span&gt; &lt;span class="nv"&gt;guide&lt;/span&gt; &lt;span class="nv"&gt;pants&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;Skin&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;, &lt;span class="nv"&gt;climb&lt;/span&gt; &lt;span class="nv"&gt;ice&lt;/span&gt;, &lt;span class="nv"&gt;switch&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;rock&lt;/span&gt;, &lt;span class="nv"&gt;traverse&lt;/span&gt; &lt;span class="nv"&gt;a&lt;/span&gt; &lt;span class="nv"&gt;knife&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Alpine&lt;/span&gt; &lt;span class="nv"&gt;wind&lt;/span&gt; &lt;span class="nv"&gt;jkt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;On&lt;/span&gt; &lt;span class="nv"&gt;high&lt;/span&gt; &lt;span class="nv"&gt;ridges&lt;/span&gt;, &lt;span class="nv"&gt;steep&lt;/span&gt; &lt;span class="nv"&gt;ice&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;anything&lt;/span&gt; &lt;span class="nv"&gt;alpine&lt;/span&gt;, &lt;span class="nv"&gt;this&lt;/span&gt; &lt;span class="nv"&gt;jac&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Ascensionist&lt;/span&gt; &lt;span class="nv"&gt;jkt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;Our&lt;/span&gt; &lt;span class="nv"&gt;most&lt;/span&gt; &lt;span class="nv"&gt;technical&lt;/span&gt; &lt;span class="nv"&gt;soft&lt;/span&gt; &lt;span class="nv"&gt;shell&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;full&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;mountain&lt;/span&gt; &lt;span class="nv"&gt;pur&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Atom&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;A&lt;/span&gt; &lt;span class="nv"&gt;multitasker&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="s"&gt;s cloud nine, the Atom plays the part of courier bag...|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Print&lt;/span&gt; &lt;span class="nv"&gt;banded&lt;/span&gt; &lt;span class="nv"&gt;betina&lt;/span&gt; &lt;span class="nv"&gt;btm&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;Our&lt;/span&gt; &lt;span class="nv"&gt;fullest&lt;/span&gt; &lt;span class="nv"&gt;coverage&lt;/span&gt; &lt;span class="nv"&gt;bottoms&lt;/span&gt;, &lt;span class="nv"&gt;the&lt;/span&gt; &lt;span class="nv"&gt;Betina&lt;/span&gt; &lt;span class="nv"&gt;fits&lt;/span&gt; &lt;span class="nv"&gt;h&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Baby&lt;/span&gt; &lt;span class="nv"&gt;micro&lt;/span&gt; &lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;luxe&lt;/span&gt; &lt;span class="nv"&gt;cardigan&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;Micro&lt;/span&gt; &lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;Luxe&lt;/span&gt; &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;a&lt;/span&gt; &lt;span class="nv"&gt;heavenly&lt;/span&gt; &lt;span class="nv"&gt;soft&lt;/span&gt; &lt;span class="nv"&gt;fabric&lt;/span&gt; &lt;span class="nv"&gt;with&lt;/span&gt; ...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Baby&lt;/span&gt; &lt;span class="nv"&gt;sun&lt;/span&gt; &lt;span class="nv"&gt;bucket&lt;/span&gt; &lt;span class="nv"&gt;hat&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;This&lt;/span&gt; &lt;span class="nv"&gt;hat&lt;/span&gt; &lt;span class="nv"&gt;goes&lt;/span&gt; &lt;span class="nv"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;when&lt;/span&gt; &lt;span class="nv"&gt;the&lt;/span&gt; &lt;span class="nv"&gt;sun&lt;/span&gt; &lt;span class="nv"&gt;rises&lt;/span&gt; &lt;span class="nv"&gt;above&lt;/span&gt; &lt;span class="nv"&gt;the&lt;/span&gt; &lt;span class="nv"&gt;horiz&lt;/span&gt;...&lt;span class="o"&gt;|&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's it; just IDs and text about the product in the form &lt;code&gt;Title -
Description&lt;/code&gt;. We're going to use a simple Natural Language Processing
technique called TF-IDF (Term Frequency - Inverse Document Frequency)
to parse through the descriptions, identify distinct phrases in each
item's description, and then find 'similar' products based on those
phrases.&lt;/p&gt;
&lt;p&gt;TF-IDF works by looking at all (in our case) one, two, and three-word
phrases (uni-, bi-, and tri-grams to NLP folks) that appear multiple
times in a description (the "term frequency") and divides them by the
number of times those same phrases appear in &lt;em&gt;all&lt;/em&gt; product
descriptions. So terms that are 'more distinct' to a particular
product ("Micro D-luxe" in item 9, above) get a higher score, and
terms that appear often, but also appear often in other products
("soft fabric", also in item 9) get a lower score.&lt;/p&gt;
&lt;p&gt;Once we have the TF-IDF terms and scores for each product, we'll use a
measurement called
&lt;a href="http://blog.christianperone.com/2013/09/machine-learning-cosine-similarity-for-vector-space-models-part-iii/"&gt;cosine similarity&lt;/a&gt;
to identify which products are 'closest' to each other.&lt;/p&gt;
&lt;p&gt;Luckily, like most algorithms, we don't have to reinvent the wheel;
there are ready-made libraries that will do the heavy lifting for
us. In this case, Python's SciKit Learn has both a
&lt;a href="http://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html"&gt;TF-IDF&lt;/a&gt;
and
&lt;a href="http://scikit-learn.org/stable/modules/metrics.html#cosine-similarity"&gt;cosine similarity&lt;/a&gt;
implementation. I've put the whole thing together in a
&lt;a href="https://github.com/groveco/content-engine"&gt;Flask app&lt;/a&gt; that will
actually serve recommendations over a REST API, as you might do in
production (in fact, the code is not very different from what we
actually do run in production at &lt;a href="https://www.grove.co"&gt;Grove&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The engine has a &lt;code&gt;.train()&lt;/code&gt; method that runs TF-IDF across the input
products file, computes similar items for every item in the set, and
stores those items along with their cosine similarity, in Redis. The
&lt;code&gt;.predict&lt;/code&gt; method just takes an item ID and returns the precomputed
similarities from Redis. Dead simple!&lt;/p&gt;
&lt;p&gt;The engine code in its entirety is below. The comments explain how the
code works, and you can explore the complete Flask app
&lt;a href="https://github.com/groveco/content-engine"&gt;on Github&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;current_app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.feature_extraction.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TfidfVectorizer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.metrics.pairwise&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;linear_kernel&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;current_app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ContentEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="n"&gt;SIMKEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;p:smlr:&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StrictRedis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;REDIS_URL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data_source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Training data ingested in &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; seconds.&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;# Flush the stale training data from redis&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flushdb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Engine trained in &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s2"&gt; seconds.&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;        Train the engine.&lt;/span&gt;

&lt;span class="sd"&gt;        Create a TF-IDF matrix of unigrams, bigrams, and trigrams&lt;/span&gt;
&lt;span class="sd"&gt;        for each product. The &amp;#39;stop_words&amp;#39; param tells the TF-IDF&lt;/span&gt;
&lt;span class="sd"&gt;        module to ignore common english words like &amp;#39;the&amp;#39;, etc.&lt;/span&gt;

&lt;span class="sd"&gt;        Then we compute similarity between all products using&lt;/span&gt;
&lt;span class="sd"&gt;        SciKit Leanr&amp;#39;s linear_kernel (which in this case is&lt;/span&gt;
&lt;span class="sd"&gt;        equivalent to cosine similarity).&lt;/span&gt;

&lt;span class="sd"&gt;        Iterate through each item&amp;#39;s similar items and store the&lt;/span&gt;
&lt;span class="sd"&gt;        100 most-similar. Stops at 100 because well...  how many&lt;/span&gt;
&lt;span class="sd"&gt;        similar products do you really need to show?&lt;/span&gt;

&lt;span class="sd"&gt;        Similarities and their scores are stored in redis as a&lt;/span&gt;
&lt;span class="sd"&gt;        Sorted Set, with one set for each item.&lt;/span&gt;

&lt;span class="sd"&gt;        :param ds: A pandas dataset containing two fields: description &amp;amp; id&lt;/span&gt;
&lt;span class="sd"&gt;        :return: Nothin!&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TfidfVectorizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analyzer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;word&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;ngram_range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                             &lt;span class="n"&gt;min_df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                             &lt;span class="n"&gt;stop_words&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;english&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tfidf_matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;description&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;cosine_similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;linear_kernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tfidf_matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tfidf_matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;similar_indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cosine_similarities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argsort&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;similar_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;cosine_similarities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;similar_indices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

            &lt;span class="c1"&gt;# First item is the item itself, so remove it.&lt;/span&gt;
            &lt;span class="c1"&gt;# This &amp;#39;sum&amp;#39; is turns a list of tuples into a single tuple:&lt;/span&gt;
            &lt;span class="c1"&gt;# [(1,2), (3,4)] -&amp;gt; (1,2,3,4)&lt;/span&gt;
            &lt;span class="n"&gt;flattened&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similar_items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIMKEY&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;flattened&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;        Couldn&amp;#39;t be simpler! Just retrieves the similar items and&lt;/span&gt;
&lt;span class="sd"&gt;        their &amp;#39;score&amp;#39; from redis.&lt;/span&gt;

&lt;span class="sd"&gt;        :param item_id: string&lt;/span&gt;
&lt;span class="sd"&gt;        :param num: number of similar items to return&lt;/span&gt;
&lt;span class="sd"&gt;        :return: A list of lists like: [[&amp;quot;19&amp;quot;, 0.2203],&lt;/span&gt;
&lt;span class="sd"&gt;        [&amp;quot;494&amp;quot;, 0.1693], ...]. The first item in each sub-list is&lt;/span&gt;
&lt;span class="sd"&gt;        the item ID and the second is the similarity score. Sorted&lt;/span&gt;
&lt;span class="sd"&gt;        by similarity score, descending.&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SIMKEY&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;num&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;withscores&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;content_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ContentEngine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Run It Yourself!&lt;/h2&gt;
&lt;p&gt;If you actually want to try this out, it's very easy. Follow the
instructions in the
&lt;a href="https://github.com/groveco/content-engine/blob/master/readme.md"&gt;readme&lt;/a&gt;
and you'll have it running locally in no time, using the sample
Patagonia data. The engine is ready to be deployed to Heroku as well.&lt;/p&gt;
&lt;p&gt;Take that, collaborative filtering! #contentbased&lt;/p&gt;</content><category term="Data Science"></category></entry><entry><title>How Celery Chord Synchronization Works</title><link href="http://blog.untrod.com/2015/03/how-celery-chord-synchronization-works.html" rel="alternate"></link><published>2015-03-26T10:20:00-07:00</published><updated>2015-03-26T10:20:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2015-03-26:/2015/03/how-celery-chord-synchronization-works.html</id><summary type="html">&lt;p&gt;Celery is a powerful tool for managing asynchronous tasks in
Python. The basic model is synchronous Python code pushes a task
(in the form of a serialized message) into a message queue (the Celery
"broker", which can be a variety of technologies - Redis, RabbitMQ,
Memcached, or even a database), and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Celery is a powerful tool for managing asynchronous tasks in
Python. The basic model is synchronous Python code pushes a task
(in the form of a serialized message) into a message queue (the Celery
"broker", which can be a variety of technologies - Redis, RabbitMQ,
Memcached, or even a database), and worker processes pull tasks off
the queue and execute them. But Celery's
&lt;a href="http://celery.readthedocs.org/en/latest/faq.html#does-celery-really-consist-of-50-000-lines-of-code"&gt;16,000&lt;/a&gt;
lines of application code certainly provide a lot more functionality
than a simple task queue. Celery exposes a number of powerful
synchronization (or "workflow" in Celery parlance)
&lt;a href="http://celery.readthedocs.org/en/latest/userguide/canvas.html"&gt;primitives&lt;/a&gt; -
ways to execute groups of tasks together, chain async task results in
a synchronous manner, or execute a callback after a group of tasks
have finished executing.&lt;/p&gt;
&lt;p&gt;At &lt;a href="https://www.grove.co"&gt;Grove&lt;/a&gt;, we make extensive use of this final
primitive, called a "chord" in Celery. Here's a trivial chord example,
from the Celery docs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;celery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chord&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tasks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tsum&lt;/span&gt;

&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;chord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;       &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))(&lt;/span&gt;&lt;span class="n"&gt;tsum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="mi"&gt;9900&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This queues up 100 'add' tasks, the results of which Celery aggregates
into a list, which is passed into the callback function (&lt;code&gt;tsum&lt;/code&gt;)
when all of the tasks have finished executing.&lt;/p&gt;
&lt;p&gt;We use chords for all sorts of tasks. For example, every night we
charge recurring shipments, and send out an internal email with the
results of how many charges succeeded, and how many failed due to
things like expired credit cards. The charge tasks are all
asynchronous, distributed across many workers, and the final email is
the chord's callback function.&lt;/p&gt;
&lt;p&gt;Pretty cool! But wait...how exactly does this coordination work? The
Celery docs leave this somewhat cryptic comment:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The synchronization step is costly, so you should avoid using chords
as much as possible. "&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Err...&lt;/p&gt;
&lt;p&gt;Reading a bit further, we learn that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;By default the synchronization step is implemented by having a
recurring task poll the completion of the group every second, calling
the signature when ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That does sound a bit costly. But wait! There's more!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is used by all result backends except Redis and Memcached, which
increment a counter after each task in the header, then applying the
callback when the counter exceeds the number of tasks in the set.
Ah-ha! The most common broker backends for Celery are Redis and
RabbitMQ, which will serve as exemplars for the two types of
synchronization as we dig in.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Redis is
&lt;a href="https://www.google.com/webhp?sourceid=chrome-instant&amp;amp;ion=1&amp;amp;espv=2&amp;amp;ie=UTF-8#q=redis%20swiss%20army%20knife"&gt;often described&lt;/a&gt;
as a "swiss army knife" for data. It functions great as a message
broker, but also as a general-purpose (non-relational) data
store. This means that Celery can store a count in Redis, of the
number of tasks originally in the chord, and increment that counter
every time a task completes. Here's the
&lt;a href="https://github.com/celery/celery/blob/master/celery/backends/redis.py#L198"&gt;relevant code&lt;/a&gt;
from the Celery source:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readycount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totaldiff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;               \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;          \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                 \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tkey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                                  \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                        \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tkey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                        \
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

     &lt;span class="n"&gt;totaldiff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totaldiff&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

     &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
         &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maybe_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
         &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;chord_size&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;totaldiff&lt;/span&gt;
         &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;readycount&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
             &lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unpack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_unpack_chord_result&lt;/span&gt;
                &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This code (which is part of a larger function) executes every time one
of the chord's subtasks completes by
&lt;a href="https://github.com/celery/celery/blob/master/celery/backends/redis.py#L100"&gt;binding&lt;/a&gt;
to the &lt;code&gt;on_chord_part_return&lt;/code&gt; property. Through some clever Redis
pipelining, the count gets incremented, then retrieved into
&lt;code&gt;readycount&lt;/code&gt;. Then, &lt;code&gt;if readycount==total&lt;/code&gt;, the chord callback
gets executed. Cool!&lt;/p&gt;
&lt;p&gt;Is it expensive, as the docs claim? Well...sort of. It causes a slew
of Redis commands to fire after every subtasks completes, but it uses
connection pooling, and the pipeline redis primitive, so there is only
one round-trip to the broker. It depends on your use case whether you
consider this expensive or not. it seems like a small price to pay for
the synchronization.&lt;/p&gt;
&lt;p&gt;Now that we understand Redis, let's see how RabbitMQ does it.&lt;/p&gt;
&lt;p&gt;RabbitMQ is a message queue. It is not designed to persist arbitrary
data, but is purpose built as a broker. There isn't a place to store a
counter, so Celery relies on a polling strategy to determine if the
chord is ready to complete. The code for this is spread out across a
number of source files, but it all starts with
&lt;a href="https://github.com/celery/celery/blob/b3d8ba2781189b7de0894f11295e815fa0bbd0b5/celery/backends/base.py#L358"&gt;apply_chord()&lt;/a&gt;
in the base broker class (which RabbitMQ, referred to as 'amqp' in
Celery,
&lt;a href="https://github.com/celery/celery/blob/b3d8ba2781189b7de0894f11295e815fa0bbd0b5/celery/backends/amqp.py"&gt;inherits from&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;When the chord starts, &lt;code&gt;apply_chord&lt;/code&gt; calls
&lt;code&gt;callback_chord_unlock&lt;/code&gt;, which in turn queues up the built-in
&lt;code&gt;celery.chord_unlock&lt;/code&gt;
&lt;a href="https://github.com/celery/celery/blob/04e77c0bd14596d8ddc9214e7cca5e817f74c9d2/celery/app/builtins.py#L59"&gt;task&lt;/a&gt;. Here's
the crucial bit of code in chord_unlock that polls for the completion
of the subtasks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
               &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;countdown&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the subtasks are ready, the callback gets queued for
execution. Otherwise, &lt;code&gt;chord_unlock&lt;/code&gt; gets queued for a
retry. So...is this expensive? Well, maybe. Certainly more so than
checking a counter. Check out how &lt;code&gt;ResultSet.ready()&lt;/code&gt;
&lt;a href="https://github.com/celery/celery/blob/5c9ee7eb72f31fca789485d5bc3a8a4f3ee7b7a7/celery/result.py#L498"&gt;works&lt;/a&gt;
-- it checks the &lt;code&gt;.ready()&lt;/code&gt; property of each subtask, which
ultimately results in a call to &lt;code&gt;_get_task_meta&lt;/code&gt; for each
subtask. Celery does a good job caching the task metadata, but
nonetheless this means examining the metadata for each task, each time
the chord_unlock polling task runs. I suspect this is what the docs
refer to when they warn of the synchronization being potentially
expensive.&lt;/p&gt;
&lt;p&gt;So that's it -- an under-the-covers look at how Celery actually
coordinates tasks. I was surprised to learn that the strategy varied
so radically based on the broker backend, but certainly reaffirms my
love for Redis and all its flexibility.&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Refactoring for Testability: A Real World Example in Python/Django</title><link href="http://blog.untrod.com/2014/04/refactoring-for-testability-quick.html" rel="alternate"></link><published>2014-04-21T22:25:00-07:00</published><updated>2014-04-21T22:25:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2014-04-21:/2014/04/refactoring-for-testability-quick.html</id><summary type="html">&lt;p&gt;At &lt;a href="https://www.epanty.com/"&gt;ePantry&lt;/a&gt;, we strive to have enough automated
test coverage that we can deploy with confidence, without being dogmatic
about our approach. We certainly aren't a TDD shop, and we don't have
rules about code coverage metrics ("no commit can reduce code
coverage!"), but it's very unusual that a non-cosmetic …&lt;/p&gt;</summary><content type="html">&lt;p&gt;At &lt;a href="https://www.epanty.com/"&gt;ePantry&lt;/a&gt;, we strive to have enough automated
test coverage that we can deploy with confidence, without being dogmatic
about our approach. We certainly aren't a TDD shop, and we don't have
rules about code coverage metrics ("no commit can reduce code
coverage!"), but it's very unusual that a non-cosmetic change makes it
to production without some sort of test coverage.&lt;/p&gt;
&lt;p&gt;But it wasn't a
straightforward journey - our team didn't have much experience with
"good" testing practices. In a few previous companies, tests if they
were required at all, were viewed as a burden; "extra work" to tack on
to your commit at the very end.&lt;/p&gt;
&lt;p&gt;So building a culture
around automated testing took time. To the extent we've succeeded, we
did it by brute force: we just wrote the damn tests. After a couple of
months of this and one by one, we built up enough actually internalize
two key lessons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It's actually &lt;em&gt;faster&lt;/em&gt; to develop if you write the tests alongside
    your code. Clicking around in the browser reproducing bugs is slow
    and unreliable and not very fun.&lt;/li&gt;
&lt;li&gt;Writing tests actually catches a &lt;em&gt;lot&lt;/em&gt; of bugs before they make it
    into production. I'm now physically uncomfortable deploying
    untested code.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But it took a bit of wandering through the testing wilderness to get there.&lt;/p&gt;
&lt;p&gt;The other day at ePantry
we refactored a bit of code for testability that I think provides a nice
example of both the mechanics and the value of automated testing -
something that I think can be tricky to understand from toy examples.
The refactoring was simple, but yielded code that is easier to maintain,
more composable, and (most importantly) allowed us to deploy a major
feature with a high degree of confidence.&lt;/p&gt;
&lt;p&gt;First, some background:&lt;/p&gt;
&lt;p&gt;At ePantry we generate
suggested shipments of household goods based on predicted consumption
habits of the household. When a customers signs up, we generate roughly
a year of suggested shipments. For instance, here's what a schedule
looks like to a user on the front end:&lt;/p&gt;
&lt;p&gt;&lt;img alt="epantry-dashboard" src="http://2.bp.blogspot.com/-Afeap6zt2Lk/U1K2rlIGoSI/AAAAAAAAAJU/1Tbe3S6zLAQ/s1600/Screen+Shot+2014-04-19+at+10.46.34+AM.png"&gt;&lt;/p&gt;
&lt;p&gt;Over time, the household
"consumes" those shipments and we need to generate more to keep the
calendar populated with future shipments. We wrote a function that would
run as an overnight chron job to "top up" the shipments of all of our
customers so each customer always has at least a year of shipments on
the calendar.&lt;/p&gt;
&lt;p&gt;Below is a code snippet
as it first appeared in our codebase, sans-tests. The code itself is
simple, but it was a bit scary to deploy into production because it runs
asynchronously, touches a lot of customers, and would not be easy to
unwind if it ran amok. I've removed some bits of logging for clarity,
but this is otherwise just as it appeared in the original commit.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_shipments_async&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;search_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;add_months&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; \
                             &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SHIPMENTS_UNTIL_AT_LEAST_MONTHS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt; \
                 &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_shipment_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pantry__shipments__arrival_date&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_shipment_date__lt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_on_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cur_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_shipment_arrival_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;cur_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;shipment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pantry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_next_shipment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cur_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shipment&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's what it does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;First we find the date in the
future that we want all customers to have shipments until. This is set
to 12 months in our settings file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next we get all of the active
customers (those who have a credit card on file) whose last shipment
arrives &lt;em&gt;before&lt;/em&gt; the cutoff date. These customers need more shipments
generated.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Finally, for each of the
customers, we loop through and create shipments for each customer until
the arrival date of the last shipment is past our target
date.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Simple! But also
untested and therefore scary. We tested it locally by cloning the
production database and running the function through the Django shell.
It &lt;em&gt;seemed&lt;/em&gt; to work. But that's not good enough. This is too important
and too complicated to be left to manual testing. So let's write some
unit tests.&lt;/p&gt;
&lt;p&gt;As is, this code is
difficult to test. It's one big function doing a bunch of stuff and we
can't test the components in isolation. If you look at the english
language explanation of the code above, there are really three distinct
steps, each of which can be separately verified. We can refactor to
represent each of those three steps:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;add_months&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; \
                      &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SHIPMENTS_UNTIL_AT_LEAST_MONTHS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_get_customers_without_enough_shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt; \
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_shipment_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pantry__shipments__arrival_date&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; \
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_shipment_date__lt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;card_on_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_create_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_shipment_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_shipment_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_create_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; \
                      &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pantry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_next_shipment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; \
                      &lt;span class="n"&gt;search_date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now what does our top-level function look like?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_shipments_async&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_create_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; \
                                   &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_shipment_arrival_date&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; \
                                   &lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; \
           &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_get_customers_without_enough_shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Piece of cake! Literally one line of code. We map our _create_until function onto the
collection of customers without enough shipments. All the detail is in
our three component functions (note that _create_until is just a
recursive loop - it could certainly be implemented in a more imperative
style of that's your thing).&lt;/p&gt;
&lt;p&gt;We just need a handful
of tests on these functions, and one good test on the high level
function, and we gain a tremendous amount of confidence and robustness
in the code. I glossed over it here, but I also discovered a few small
bugs in our Django ORM query while writing the tests. Here are a few
example tests, using Django's test framework and Factory Boy to create
mock objects.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;django.test&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TestCase&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pypantry.tests.factories&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PantryFactory&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;schedule.tests.factories&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ShipmentFactory&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pypantry.tasks&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;utils.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add_months&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestShipmentGenTask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PantryFactory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_card_on_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;foo&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShipmentFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pantry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pantry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_finds_customers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_customers_without_enough_shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ignores_customers_with_enough_shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_get_customers_without_enough_shipments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_shipments_get_created&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;15042014&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;%m%Y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;_create_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;add_months&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="c1"&gt;# one extra because it goes one shipment &amp;quot;past&amp;quot; the target date&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arrival_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_shipments_until_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;create_shipments_async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shipments&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ta-da! Of course there
are plenty of other tests you could write, but just these four basic
tests exercise the code reasonably well. And it would have been
virtually impossible to get this level of testing fidelity with the
original function. Plus, if one of these tests fails due to a later
commit, we can immediately hone in on the cause. The refactoring and
tests took less than an hour, and it will save me at least that much
worry.&lt;/p&gt;
&lt;p&gt;I hope you found this a
useful, real world example of refactoring for testability. Love to hear
any additional thoughts in the comments!&lt;/p&gt;
&lt;p&gt;P.S. This code made it
into production yesterday and ran successfully for the first time last
night. Phew! Also -- you should sign up for
&lt;a href="https://www.epantry.com/"&gt;ePantry&lt;/a&gt; and never run out of soap or toilet
paper ever again.&lt;/p&gt;</content><category term="code &amp; tutorials"></category></entry><entry><title>The No-Frills Guide to PGP on OS X</title><link href="http://blog.untrod.com/2013/11/the-no-frills-guide-to-pgp-on-mac-os-x.html" rel="alternate"></link><published>2013-11-01T00:09:00-07:00</published><updated>2013-11-01T00:09:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-11-01:/2013/11/the-no-frills-guide-to-pgp-on-mac-os-x.html</id><summary type="html">&lt;p&gt;Want to get started using PGP on your Mac, but confused by the morass of
&lt;a href="http://contemporary-home-computing.org/prof-dr-style/"&gt;professor-doctor&lt;/a&gt;
style sites with seemingly out-of-date software and plugins? Have no
fear! I'll have you up and running in minutes with this handy guide.
You'll learn how to use public key servers and how to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Want to get started using PGP on your Mac, but confused by the morass of
&lt;a href="http://contemporary-home-computing.org/prof-dr-style/"&gt;professor-doctor&lt;/a&gt;
style sites with seemingly out-of-date software and plugins? Have no
fear! I'll have you up and running in minutes with this handy guide.
You'll learn how to use public key servers and how to encrypt and send
and receive and decrypt emails, and how to sign and verify messages
using GPG. By the time you're done, you might actually find the GPG
manual pages for &lt;a href="http://gnupg.org/gph/en/manual/x135.html"&gt;simple
tasks&lt;/a&gt; illuminating instead of
totally incoherent.&lt;/p&gt;
&lt;p&gt;Really explaining how public key encryption works or why you'd want to
use it is not the goal of this post by a long shot - this is just a
simple block-and-tackle tutorial on how to use the stuff. If you want an
easy-to-read, non-technical intro to asymmetric cryptography, go read
the &lt;a href="http://www.jus.uio.no/sisu/little_brother.cory_doctorow/10.html"&gt;first section of chapter
10&lt;/a&gt; of
Little Brother by Cory Doctorow. But with that said, I'd be remiss if I
didn't at least provide...&lt;/p&gt;
&lt;h3&gt;Part 0 - A Two-Sentence Introduction to Public Key Encryption&lt;/h3&gt;
&lt;p&gt;Everyone who wants to communicate securely generates their own pair of
keys, one of which they publicize, and one of which they keep private.
To send someone a secure message, just encrypt your message with the
recipient's public key, and that person (and only that person) will be
able to decrypt it using their private key.&lt;/p&gt;
&lt;p&gt;Got it?&lt;/p&gt;
&lt;h3&gt;Part 1 - A Quick Disambiguation of PGP, OpenPGP, and GPG&lt;/h3&gt;
&lt;p&gt;PGP stands for Pretty Good Privacy and is a piece of software that
implements the OpenPGP public key (or asymmetric) encryption
&lt;a href="http://tools.ietf.org/html/rfc4880"&gt;standard&lt;/a&gt;. The PGP implementation
is owned by Symantec. GPG is another, free, implementation of OpenPGP
that stands for Gnu Privacy Guard. GPG is very common on *nix systems
and it's what we'll use here. So basically GPG and PGP are functionally
equivalent.&lt;/p&gt;
&lt;h3&gt;Part 2 - Installing Stuff&lt;/h3&gt;
&lt;p&gt;We're going to use a Thunderbird (which is a great email client by
Mozilla) extension called Enigmail to send and receive GPG emails. Let's
get installing!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Get &lt;a href="http://www.mozilla.org/en-US/thunderbird/"&gt;Thunderbird&lt;/a&gt; and
    hook it up to an email account you use. Thunderbird is pretty great,
    and &lt;a href="https://support.mozillamessaging.com/en-US/kb/thunderbird-and-gmail"&gt;works well with
    Gmail&lt;/a&gt; (you
    only need to do the super short instructions in the "configuring
    your gmail" section - it's that easy). Ok done? Good.&lt;/li&gt;
&lt;li&gt;Now we need to install GPG. For OS X, you'll want &lt;a href="https://gpgtools.org/#gpgsuite"&gt;GPG
    Suite&lt;/a&gt;. It's super easy to install,
    and will walk you through creating your first GPG key pair just
    after installation. It will default to an RSA key of 2048 bits, but
    I'd recommend using 4096. It's quite a bit more secure and really
    doesn't have any downsides - it's a bit more computationally
    expensive, but that doesn't matter unless you're using the key for
    something like SSL. We're just encrypting emails here.&lt;/li&gt;
&lt;li&gt;Finally, &lt;a href="https://addons.mozilla.org/en-us/thunderbird/addon/enigmail/"&gt;install
    Enigmail&lt;/a&gt;.
    This is a Thunderbird extension that provides simple integration
    with GPG so you don't have to muck about with a bunch of command
    line tools just to deal with sending and receiving email. There are
    a few configuration options - odds are, unless you are a character
    in
    &lt;a href="http://www.goodreads.com/book/show/816.Cryptonomicon"&gt;Cryptonomicon&lt;/a&gt;, you
    don't want to encrypt or sign your messages by default. The
    &lt;a href="https://www.enigmail.net/home/index.php"&gt;official homepage&lt;/a&gt; of
    Engimail makes it look a little long in the tooth but it works great
    with the latest version of Thunderbird.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Part 3 - Using Key Servers&lt;/h3&gt;
&lt;p&gt;Now that you have a key pair created, you need to share it with the
world - so that anyone who wants to communicate with you securely can
encrypt messages to you with your public key. There are a number of
organizations that maintain public keyservers to do just this. Key
servers are just searchable directories of public keys. I use the
&lt;a href="http://pgp.mit.edu/"&gt;MIT's&lt;/a&gt; because &lt;a href="http://www.mit.edu/~prz/EN/background/index.html"&gt;Phil
Zimmerman&lt;/a&gt;, the
inventor of PGP, is an MIT guy, and the server has been around a heck of
a long time.&lt;/p&gt;
&lt;p&gt;Go ahead and &lt;a href="http://pgp.mit.edu:11371/pks/lookup?search=chris%40untrod.com&amp;amp;op=index"&gt;search for
me&lt;/a&gt;.
That's my key!&lt;/p&gt;
&lt;p&gt;If you click on the key, you'll actually see the long block of gibberish
that is my public RSA key. But you don't even have to interact with that
- the GPG Suite makes it super easy to publish and install keys public
keys. In the GPG Keychain Access tool (under Applications on your Mac),
go to Apple &amp;gt; Preferences and point to the MIT key server:&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp-1.png"&gt;&lt;/p&gt;
&lt;p&gt;Now you can publish your public key to the server by right-clicking your
key and hitting "Send public key to Keyserver":&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp0.png"&gt;&lt;/p&gt;
&lt;p&gt;Now go &lt;a href="http://pgp.mit.edu/"&gt;search&lt;/a&gt; for your email on MIT's servers and
you can find your public key! Not only that, MIT's key server will
propagate your key to other keyservers all around the world.&lt;/p&gt;
&lt;p&gt;Installing keys is just as easy. You can install mine by going to Key
&amp;gt; Retrieve From Keyserver and putting in my key ID from MIT's server
(0x6f0eff6b2e0593ad). That's me! And now the world can find your public
key just as easily.&lt;/p&gt;
&lt;h3&gt;Part 4 - Sending &amp;amp; Receiving Encrypted Messages&lt;/h3&gt;
&lt;p&gt;With Engimail installed, this is really pretty easy. Open Thunderbird
and compose a new message. You'll see the OpenPGP drop down menu at the
top, and you can elect to encrypt the message.&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp1.png"&gt;&lt;/p&gt;
&lt;p&gt;When you go to send the message, you'll of course have to select the
public key with which to encrypt the message. Enigmail will detect the
key automatically if you have a key with a matching email address on
file with your GPG Keychain already, but if not you'll have to select
one (or, more likely, go retrieve and install the correct one from a
keyserver).&lt;/p&gt;
&lt;p&gt;So if you write me a message like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp3.png"&gt;&lt;/p&gt;
&lt;p&gt;And choose to encrypt it with my private key, here is the email that
actually gets sent (note that the subject line is &lt;strong&gt;&lt;em&gt;NOT&lt;/em&gt;&lt;/strong&gt; encrypted):&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp2.png"&gt;&lt;/p&gt;
&lt;p&gt;And if I open it in Thunderbird, Engimail detects the encryption,
prompts for my password, decrypts it, and lets me know that it's
decrypted a message:&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp4.png"&gt;&lt;/p&gt;
&lt;p&gt;Couldn't be easier! The harder thing is actually finding someone who
also knows how to send and receive encrypted emails. Hah hah.&lt;/p&gt;
&lt;h3&gt;Part 5 - Signing &amp;amp; Verifying Emails&lt;/h3&gt;
&lt;p&gt;Besides keeping communication private, public key cryptography has
another superpower - identify verification. The properties of the
private/public key pairs means that, not only can someone else use your
public key to encrypt a message that is only decryptable by you, but you
can encrypt a message using your private key that can only be decrypted
by your public key. Now, you may be asking yourself "why is that useful?
Why bother encrypting a message that anyone can then read?". Well, if
the message can be decrypted using your public key, then it &lt;em&gt;must&lt;/em&gt; have
been signed using your private key - which means you must have sent the
message! Thus we can exploit public key cryptography for identity
verification, as well as secret-message sending.&lt;/p&gt;
&lt;p&gt;A signed message looks like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;-----BEGIN
PGP SIGNED MESSAGE-----
Hash: SHA512
Really!
-----BEGIN PGP SIGNATURE-----
Version: GnuPG/MacGPG2 v2.0.22 (Darwin)
Comment: GPGTools - https://gpgtools.org
Comment: Using GnuPG with Thunderbird - http://www.enigmail.net/
iQIcBAEBCgAGBQJSmOyIAAoJEG8O/2suBZOtmSUQAMSEbn13rjey0L9trG/Qc3eI
DEvbnl1Vkx+37d7o8GVtwqmD5uwfh2RphyRq/l1ML3fz00pFzmMH7mfNib6BVZ9U
nMXkN+r3x2VUUCNcYbn6i3lXWkxNwYiUIiUQpgwj9DKKv3n7ujNxC/u/1d+dcVK4
hlrf1oHCMMPIcqyQKDRKkPNOB9LGm1Y1KlKFKA6C/ElEAn/48kJKlCKZA55VJSk2
TuxutrduaPOkPjuY9zBGHWlcfF5d1CtZaQALPxjcBbS0z9mSW4vqJRO4kRUY7SAV
jWFBfmPKNVPcZQTqvPd6CfqyQqmgCnl1fIHThHviWAMbX3GdGjsscRSzC3DiJRgZ
cMcXLqjlqWYaml/6Iq8V8+Azk6Ph2ORxZOsKDiOAz+VwZRQwlyjMu9SSkA/VABEY
dN7OrNZZwTwz2A1/QH/SHNvRVAB3kLRpzJTaLJsuFBeh5aycjjbhETHtYccYsxDf
rmuGnnfxBrglkbBaYExxzKutaE/yVeFLSegO9clxa2biSk31X51kOjS/2Vy8UQgd
iVRNQb/3ArfjOFiQvXIylGkJS0aiVmJXrkEOyiSzy5h0JGxpa2T4JWZ9VyrpGzLx
8PJUPPcYUGcfTEcB0dRvBC7/GpTn/ChEcMBfrPAqI+srsPG0CIBp+aIDAQEsSH61
tWVnIgeDuHCOFrNrB3bK
=SYaF
-----END PGP SIGNATURE-----&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Under the hood, the signature was generated by hashing the
message contents (in this case you can see Enigmail inserted the hashing
algorithm it used - SHA512), then using the sender's private key to
encrypt the hash along with a timestamp. The recipient can then verify
the message by decrypting the signature blog, re-hashing the contents,
and comparing. Not only is the sender's identity verified, but so are
the message contents and the sending time. Cool!&lt;/p&gt;
&lt;p&gt;Sending and receiving signed messages through Engimail is just as easy
as sending and receiving encrypted messages - just select the option
when sending, and Enigmail handles everything for you. Signed messages
are automatically verified. Here's what the above message looks like
when viewed in Engimail:&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp5.png"&gt;&lt;/p&gt;
&lt;p&gt;Sometimes you might want to verify blocks of text (or an email you
received through a client other than Thunderbird). That's really simple
with GPG as well, though you'll have to go to the command line. To try
it out for yourself, save the above signed message as test.txt and then
run the following from a terminal window:&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp6.png"&gt;&lt;/p&gt;
&lt;p&gt;Ta-da! Piece of cake.&lt;/p&gt;
&lt;h4&gt;A Quick Digression About Trusted Signatures&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;You'll see an ominous WARNING in the gpg output above, letting you
know that this is not a trusted signature. This simply means that you
have not indicated that you trust the source of the public key you
used to decrypt the message. For instance, what if I am not Chris
Clark at all? But some nefarious impersonator who has broken into
Chris' blog and provided a public key in this post that is not Chris'
at all? What then?? Who &lt;strong&gt;can&lt;/strong&gt; we trust?&lt;/p&gt;
&lt;p&gt;Turns out this is actually a pretty hard problem to solve. The PGP
community has a concept called the &lt;a href="http://en.wikipedia.org/wiki/Web_of_trust"&gt;web of
trust&lt;/a&gt;, which you can read
about on your own. Another approach is &lt;a href="http://en.wikipedia.org/wiki/Key_signing_party"&gt;key signing
parties&lt;/a&gt;. Ultimately
you will need to make the trust determination on your own. You can
then set your trust level for each public key in the GPG Keychain
tool. By default all keys except your own are "undefined", and this
warning won't go away until you've indicated you have "ultimate" trust
in the key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;Back to verification&lt;/h4&gt;
&lt;p&gt;For kicks, try modifying the message slightly -
replace my "Really!" with "Really." and re-run the verification (also
note that I've now set my own key back to "Ultimate" trust so the
warning is gone):&lt;/p&gt;
&lt;p&gt;&lt;img alt="first-screen" src="http://blog.untrod.com/images/pgp7.png"&gt;&lt;/p&gt;
&lt;p&gt;And that's it! You know how to use the GPG tool to communicate securely.
There are all sorts of intricacies and details that are loads of fun to
learn about, and you should now have a bit of a foundation to go
exploring. Maybe those &lt;a href="http://gnupg.org/gph/en/manual/x135.html"&gt;manual
pages&lt;/a&gt; aren't so cryptic now
after all. Good luck, and leave a comment if you'd like to exchange some
encrypted emails with yours truly. You know where to &lt;a href="http://pgp.mit.edu:11371/pks/lookup?op=get&amp;amp;search=0x6F0EFF6B2E0593AD"&gt;find
me&lt;/a&gt;.&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Form Over Function &amp; The New iPhone</title><link href="http://blog.untrod.com/2013/09/form-over-function.html" rel="alternate"></link><published>2013-09-12T05:57:00-07:00</published><updated>2013-09-12T05:57:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-09-12:/2013/09/form-over-function.html</id><summary type="html">&lt;p&gt;I probably found this more profound than it really is, but Matt Buchanan
&lt;a href="http://www.newyorker.com/online/blogs/elements/2013/09/the-wonderfully-mundane-new-iphone.html"&gt;said it
well&lt;/a&gt;
in the New Yorker, regarding the latest iPhone announcements:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fundamental technology, like manufacturing processes for processors
and imaging sensors and displays, have evolved to the point that the
basic shape and sense of a …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;I probably found this more profound than it really is, but Matt Buchanan
&lt;a href="http://www.newyorker.com/online/blogs/elements/2013/09/the-wonderfully-mundane-new-iphone.html"&gt;said it
well&lt;/a&gt;
in the New Yorker, regarding the latest iPhone announcements:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Fundamental technology, like manufacturing processes for processors
and imaging sensors and displays, have evolved to the point that the
basic shape and sense of a phone—a thin rectangle with a
four-to-five-inch high-resolution touch screen stuffed with a variety
of sensors—is determined now largely based on its merits rather than
its outright technical limitations, much the same way that the basic
shape of a knife is defined by its function rather than our ability to
produce it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's amazing to think that this is true; that the technological
achievements of these devices are perhaps being held back more by the
size and shape of our hands than by our manufacturing capabilities.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Declarative Approach to Nesting Backbone Models</title><link href="http://blog.untrod.com/2013/08/declarative-approach-to-nesting.html" rel="alternate"></link><published>2013-08-01T23:57:00-07:00</published><updated>2013-08-01T23:57:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-08-01:/2013/08/declarative-approach-to-nesting.html</id><summary type="html">&lt;p&gt;Backbone doesn't have great (any?) support for nested models. Here's my
approach. I think it's kind of fun (we get to write recursive
functions!), and hopefully useful. I'll report back when I've lived with
it for a bit longer.&lt;/p&gt;
&lt;p&gt;Here's the problem: We have a Backbone model with attributes that …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Backbone doesn't have great (any?) support for nested models. Here's my
approach. I think it's kind of fun (we get to write recursive
functions!), and hopefully useful. I'll report back when I've lived with
it for a bit longer.&lt;/p&gt;
&lt;p&gt;Here's the problem: We have a Backbone model with attributes that ought
to be other Backbone models, and some of those models' attributes ought
to also be Backbone models. We want Backbone models all the way down!
But when our REST endpoint returns data from the server, Backbone
doesn't know that and hydrates only the top-level model to a first-class
Backbone object. So we end up with ugly code inside &lt;code&gt;_.each&lt;/code&gt; loops calling
&lt;code&gt;new App.Models.MyModel(data)&lt;/code&gt; all over the place. It's gross.&lt;/p&gt;
&lt;p&gt;More concretely, after we &lt;code&gt;fetch()&lt;/code&gt;
a model (in this example, we'll use a Customer model with some nested
data), we typically get a data structure back that looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nv"&gt;attributes : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;name : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;email : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;address : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;shipments : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;date : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;total : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;items : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;date : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;total : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;items : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But I actually have a bunch of Models and Collections that correspond to
various pieces of my Customer model, so what I really want is this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;customer : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nv"&gt;attributes : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;name : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;email : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;address : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;shipments : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nv"&gt;models : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nv"&gt;attributes : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nv"&gt;date : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nv"&gt;total : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nv"&gt;items : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
              &lt;span class="nv"&gt;Models : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Shipment&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nv"&gt;attributes : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="nv"&gt;date : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nv"&gt;total : &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nv"&gt;items : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
              &lt;span class="nv"&gt;models : &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In other words, I want to walk down my root object, and map models and
collections onto it where appropriate (we aren't trying to turn
EVERYTHING into a Backbone object, just the objects we have models and
collections for). You can of course just write a bunch of loops and do
it by hand, but I've tried to great a more general purpose method that
can be reused across an app. Basically, each model can provide a "map"
of how it should be rehydrated into a complete, nested model when it
returns from the server. So let's write that (all examples are in
Coffeescript):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;

  &lt;span class="nv"&gt;map: &lt;/span&gt;&lt;span class="nf"&gt;() -&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name: &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;obj: &lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Address&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;name: &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;shipments&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;obj: &lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Shipments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;children: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name: &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;items&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;obj: &lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ShipmentItems&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pretty simple! Using standard JSON we declare which attributes we want
to turn into Backbone objects, and provide an reference to the relevant
class. For deeper nesting, we just specify the child mappings in the
same manner. With this simple pattern, we can easily write arbitrarily
complex mappings, with deep nesting. Also, Backbone is helpful during
the Collection instantiation process and converts the contents of an
array into Backbone models, which is why we can stop the map at the
Collections level (although we could go deeper).&lt;/p&gt;
&lt;p&gt;Note also that map is an anonymous function, rather than just a
property. This is because we want to evaluate map at the time we hydrate
the object to ensure that all of the relevant Backbone types are
loaded.&lt;/p&gt;
&lt;p&gt;Now we need a way to walk the Customer model and apply the mapping.
We'll write a helper function called "hydrate" for that, which will
recursively walk the map and hydrate the objects:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;App.hydrate = &lt;/span&gt;&lt;span class="nf"&gt;(root, map) =&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;
    &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;(field) -&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;
        &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isArray&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;
        &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;(i) -&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt;
        &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's look at this in a bit more depth because I am barely smart enough
to write recursive functions and it will give me confidence that I did
it correctly if I can explain it to you.&lt;/p&gt;
&lt;p&gt;First, if the root object is already a Backbone object, we'll walk the
attributes instead of the Model itself. This is mostly a convenience
thing so that we can call &lt;code&gt;hydrate(model)&lt;/code&gt;
instead of &lt;code&gt;hydrate(model.attributes)&lt;/code&gt;
but it also adds a bit of robustness in case you're doing something
weird and call hydrate on a model where some, but not all, of the nest
models are already Backbone objects. This'll still work and just hydrate
the ones that still need it. I don't have a guard here against
already-hydrated Collections, but it would be easy enough to add one.&lt;/p&gt;
&lt;p&gt;The function then walks through each field in the map, recursively
calling hydrate if there are any child maps present. Once we've reach
the bottom of the map, the function checks whether the object is an
array (in which case each item needs to be hydrated), or just a simple
object (in which case just the target field needs to be hydrated).&lt;/p&gt;
&lt;p&gt;This version of the function isn't tail-recursive because JavaScript VMs
don't have tail-call optimization so there's not point. It's easy to
flip the order of the field.children check though, and make it tail
recursive for &lt;a href="http://wiki.ecmascript.org/doku.php?id=harmony:proper_tail_calls"&gt;when ECMAScript 6 comes
out&lt;/a&gt;.
The tail-recursive version of the function is just a little tricker to
explain, so I opted for this one.&lt;/p&gt;
&lt;p&gt;Ok - got all that? The next step is to actually call the darn thing!
Because I'm in an auto-magical mood, we'll stick on the model's &lt;code&gt;parse()&lt;/code&gt;
method. Parse fires after every &lt;code&gt;fetch()&lt;/code&gt; and
&lt;code&gt;save()&lt;/code&gt; of the model, so we're guaranteed to get the hydrated version back after
every server call. The full customer class thus looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Customer&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Backbone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;

  &lt;span class="nv"&gt;map: &lt;/span&gt;&lt;span class="nf"&gt;() -&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;

  &lt;span class="nv"&gt;parse: &lt;/span&gt;&lt;span class="nf"&gt;(response) -&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hydrate&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;@map&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And voila! Every time we sync from the server, the model will hydrate
itself! Neato!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Edit&lt;/strong&gt;: After writing this, I found &lt;a href="http://stackoverflow.com/a/9904874/221390"&gt;this
answer&lt;/a&gt; on Stackoverflow,
which is very similar, and I like a lot. In some situations I like my
approach because there is a single map of the entire nesting model in
one place, on the root model, so it's very quick to get your head around
the model relationships. On the other hand, rycfung's approach is nicely
encapsulated and the parsing code is much simpler. And there is a lot to
be said for simple code when you are up late at night debugging
recursive functions.&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>How to Write a Bug Report</title><link href="http://blog.untrod.com/2013/07/how-to-write-bug-report.html" rel="alternate"></link><published>2013-07-11T00:32:00-07:00</published><updated>2013-07-11T00:32:00-07:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2013-07-11:/2013/07/how-to-write-bug-report.html</id><summary type="html">&lt;p&gt;Writing good bug reports is the difference between actually seeing your
bug get fixed and sending protracted emails over the course of a week
convincing a developer that there is, in fact, a bug. By far the most
important information you can provide in a bug report are clear
reproduction …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Writing good bug reports is the difference between actually seeing your
bug get fixed and sending protracted emails over the course of a week
convincing a developer that there is, in fact, a bug. By far the most
important information you can provide in a bug report are clear
reproduction steps. Here's my guidance on how to write these:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Pretend you are writing to someone who has a &lt;em&gt;very&lt;/em&gt; basic understanding of the product.&lt;/li&gt;
&lt;li&gt;Now also pretend that this person is an idiot.&lt;/li&gt;
&lt;li&gt;Tell them, step-by-step, how to reproduce the issue.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So good reproduction steps look like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;Log in as user test@test.com with password "foo" on the live website&lt;/li&gt;
&lt;li&gt;View the dashboard&lt;/li&gt;
&lt;li&gt;Note the dollar amount in the "Your subscription plan" area in the
lower left&lt;/li&gt;
&lt;li&gt;Hit the "pay" button next to that amount&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Expected Result&lt;/strong&gt;
"Today's Payment" is populated in step 1 of the checkout form&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Actual Result&lt;/strong&gt;
"Today's Payment" is blank&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes this can be a little tedious, but if you get in the habit of doing
it, it only takes like 30 seconds more per bug, and if it takes longer
it's because it's forcing you to actually reproduce the thing
consistently, which is the whole point in the first place.&lt;/p&gt;
&lt;p&gt;And using this numbered, step-wise, idiot-proof format will cut down on
the back-and-forth between you and whoever is fixing the bug by an order
of magnitude. So it's ultimately a huge time saver and a great habit to
get into. You life will contain 25% fewer bugs and developers will hug
you.&lt;/p&gt;
&lt;p&gt;Bonus tip: Whenever possible, provide screenshots!&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Changing Our Development Process</title><link href="http://blog.untrod.com/2013/07/changing-our-development-process.html" rel="alternate"></link><published>2013-07-11T00:22:00-07:00</published><updated>2013-07-11T00:22:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-07-11:/2013/07/changing-our-development-process.html</id><summary type="html">&lt;p&gt;This is a post about change management at a start-up.&lt;/p&gt;
&lt;p&gt;Kaggle, at just under 20 people, is an interesting size because we are
right on the cusp of being able to wing it in terms of process and
communication, and needing more formal processes to make sure changes
and plans …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a post about change management at a start-up.&lt;/p&gt;
&lt;p&gt;Kaggle, at just under 20 people, is an interesting size because we are
right on the cusp of being able to wing it in terms of process and
communication, and needing more formal processes to make sure changes
and plans are communicated to everyone who needs to know.&lt;/p&gt;
&lt;p&gt;For a while our engineering team was just winging it - we used Asana (a
sophisticated to-do list application) to assign and track tasks, but
didn't have much estimation rigor, work would get scheduled "just in
time", and priorities were constantly changing. While this roughly
worked, and product was going out the door, we could have been doing
better on a few fronts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Productivity. With better prioritization and specification,
    task-switching time drops, and there are fewer questions to answer
    while an item is in flight.&lt;/li&gt;
&lt;li&gt;Morale. With an ad-hoc process it's very easy to agree to deliver a
    new feature without a clear understanding of what's getting
    traded off. This leads to misset expectations, or poorly
    communicated changes and stresses everyone out.&lt;/li&gt;
&lt;li&gt;Predictability. People are crummy at estimating how long complex
    tasks will take. So it's no surprise that, lacking good estimation
    techniques, we were consistently inaccurate estimating delivery
    dates for features.&lt;/li&gt;
&lt;li&gt;Transparency. Asana is great for many things, but it was ultimately
    too flexible and inconsistent for anyone to look to it as a single
    source of truth for all development activities. So instead, they
    would ask me or individual engineers when something is going
    to ship.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We wanted a process that addressed these issues, without creating a
bunch of bureaucracy that is anathema to start-ups (or, as one of our
engineers put it, "better to manage, but worse"). I'll detail the
process itself in a future post, but wanted to discuss here how we
managed the change.&lt;/p&gt;
&lt;p&gt;There were two basic approaches we could take:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make incremental changes, learning along the way, and evolve a
    process the whole team was happy with. We could research and try a
    few different tools, experiment with stand-ups, and ultimately
    figure out what makes the most sense for our team&lt;/li&gt;
&lt;li&gt;Make a sudden, larger, opinionated change and tweak from there. Do a
    big-bang roll-out of an agile development process and incorporate
    the agile tools I've come to really like over the years.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went for option 2. Management strong-arming is not likely to go over
well with any engineering team, but I thought it actually made sense in
this case for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The developers on the team had either good experiences with agile,
    or no experience with agile; there was no scar tissue.&lt;/li&gt;
&lt;li&gt;While our company culture is contemplative, flat, and autonomous, we
    also place a lot of value on just trying stuff quickly.&lt;/li&gt;
&lt;li&gt;I had earned enough political capital as an engineering manager that
    I the team would give it a fair shake even if I was being a
    dictatorial jerk. In fact, I had tried to do something like this
    much earlier in my tenure at Kaggle and the team pushed back because
    I hadn't earned credibility yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And crucially, during the roll-out meeting, I explained that yes, I am
being unilateral here, but here's why I think it makes sense anyway, and
here is the meeting we are having in 3 weeks to evaluate how you like
it, and at that same meeting you can decide to blow it all up if you
hate it.&lt;/p&gt;
&lt;p&gt;The final piece of the puzzle was to make it &lt;em&gt;feel&lt;/em&gt; like a real change.
When changing largely invisible things like process there's the risk
that, in spite of the change, people roughly go back to doing roughly
what they were doing before, just with different window dressing. To
make it feel tangible and different, we mounted a big TV on the wall
that displayed our current sprint, rearranged the floor plan to give a
clear sense of space to the team, and made sure out our tracking tool
(Pivotal) was integrated with our company chat room on day one so
everyone would seem the stream of activity generated by the new process.&lt;/p&gt;
&lt;p&gt;And bang! On Friday we were in ad-hoc winging-it land, and on Monday we
were starting our first sprint, sitting in our first sizing meeting,
with Fibonacci planning poker cards in front of us.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Getting Started with Pandas - Predicting SAT Scores for New York City Schools</title><link href="http://blog.untrod.com/2013/01/getting-started-with-pandas-predicting.html" rel="alternate"></link><published>2013-01-23T01:20:00-08:00</published><updated>2013-01-23T01:20:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-01-23:/2013/01/getting-started-with-pandas-predicting.html</id><summary type="html">&lt;h2&gt;An Introduction to Pandas&lt;/h2&gt;
&lt;p&gt;This tutorial will get you started with Pandas - a data analysis library for Python that is great for data preparation, joining, and ultimately generating well-formed, tabular data that's easy to use in a variety of visualization tools or (as we will see here) machine learning applications …&lt;/p&gt;</summary><content type="html">&lt;h2&gt;An Introduction to Pandas&lt;/h2&gt;
&lt;p&gt;This tutorial will get you started with Pandas - a data analysis library for Python that is great for data preparation, joining, and ultimately generating well-formed, tabular data that's easy to use in a variety of visualization tools or (as we will see here) machine learning applications. This tutorial assumes a solid understanding of core Python functionality, but nothing about machine learning or Pandas.&lt;/p&gt;


&lt;h3&gt;Goals&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Using data from [NYC Open Data] (https://data.cityofnewyork.us/), build a unified, tabular dataset ready for use with machine learning algorithms to predict student SAT scores on a per school basis.&lt;/li&gt;
&lt;li&gt;Learn and use the Pandas data analysis package.&lt;/li&gt;
&lt;li&gt;Learn how data is typically prepared for machine learning algorithms (ingestion, cleaning, joining, feature generation).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;First, let's ingest the data and get the lay of the land. You can download the data sets referenced below from &lt;a href="https://nycopendata.socrata.com/"&gt;NYC Open Data&lt;/a&gt;, or directly download a &lt;a href="http://blog.untrod.com/files/NYC_Schools.zip"&gt;zip file&lt;/a&gt; with the relevant data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;

&lt;span class="c1"&gt;# Load the data&lt;/span&gt;
&lt;span class="n"&gt;dsProgReports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C:/data/NYC_Schools/School_Progress_Reports_-_All_Schools_-_2009-10.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsDistrict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C:/data/NYC_Schools/School_District_Breakdowns.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C:/data/NYC_Schools/2009-10_Class_Size_-_School-level_Detail.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C:/data/NYC_Schools/School_Attendance_and_Enrollment_Statistics_by_District__2010-11_.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;#last two rows are bad&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;C:/data/NYC_Schools/SAT__College_Board__2010_School_Level_Results.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Dependent&lt;/span&gt;

&lt;span class="n"&gt;dsSATs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;lt;class &amp;#39;pandas.core.frame.DataFrame&amp;#39;&amp;gt;
Int64Index: 460 entries, 0 to 459
Data columns:
DBN                      460  non-null values
School Name              460  non-null values
Number of Test Takers    460  non-null values
Critical Reading Mean    460  non-null values
Mathematics Mean         460  non-null values
Writing Mean             460  non-null values
dtypes: object(6)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Outline&lt;/h2&gt;
&lt;p&gt;Pandas has read the data files without issue. Next let's create a rough map of where we are going.&lt;/p&gt;
&lt;p&gt;We have five datasets here, each with information about either schools or districts. We're going to need to join all of this information together into a tabular file, with one row for each school, joined with as much information we can gather about that school &amp;amp; its district, including our dependent variables, which will be the mean SAT scores for each school in 2010.&lt;/p&gt;
&lt;p&gt;Drilling down one level of detail, let's look at the dataset dsSATs, which contains the target variables:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;dsSATs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;lt;class &amp;#39;pandas.core.frame.DataFrame&amp;#39;&amp;gt;
Int64Index: 460 entries, 0 to 459
Data columns:
DBN                      460  non-null values
School Name              460  non-null values
Number of Test Takers    460  non-null values
Critical Reading Mean    460  non-null values
Mathematics Mean         460  non-null values
Writing Mean             460  non-null values
dtypes: object(6)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Target Variable and Joining Strategy&lt;/h2&gt;
&lt;p&gt;We are going to build a dataset to predict Critical Reading Mean, Mathematics Mean, and Writing Mean for each school (identified by DBN).&lt;/p&gt;
&lt;p&gt;After digging around in Excel (or just taking my word for it) we identify the following join strategy (using SQL-esque pseudocode):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;dsSATS&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL CODE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;join&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;join&lt;/span&gt; &lt;span class="n"&gt;dsDistrct&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;join&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;District&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we have the strategy identified at a high level, there are a number of details we have to identify and take care of first.&lt;/p&gt;
&lt;h2&gt;Primary Keys - Schools&lt;/h2&gt;
&lt;p&gt;Before we can join these three datasets together, we need to normalize their primary keys. Below we see the mismatch between the way the DBN (school id) field is represented in the different datasets. We then write code to normalize the keys and correct this problem.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL CODE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div style="max-height:1000px;max-width:1500px;overflow:auto;"&gt;
&lt;table border="1" class="dataframe"&gt;
  &lt;thead&gt;
    &lt;tr style="text-align: right;"&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;0&lt;/th&gt;
      &lt;th&gt;1&lt;/th&gt;
      &lt;th&gt;2&lt;/th&gt;
      &lt;th&gt;3&lt;/th&gt;
      &lt;th&gt;4&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DBN&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; 01M015&lt;/td&gt;
      &lt;td&gt; 01M019&lt;/td&gt;
      &lt;td&gt; 01M020&lt;/td&gt;
      &lt;td&gt; 01M034&lt;/td&gt;
      &lt;td&gt; 01M063&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DBN&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; 01M292&lt;/td&gt;
      &lt;td&gt; 01M448&lt;/td&gt;
      &lt;td&gt; 01M450&lt;/td&gt;
      &lt;td&gt; 01M458&lt;/td&gt;
      &lt;td&gt; 01M509&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;SCHOOL CODE&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;   M015&lt;/td&gt;
      &lt;td&gt;   M015&lt;/td&gt;
      &lt;td&gt;   M015&lt;/td&gt;
      &lt;td&gt;   M015&lt;/td&gt;
      &lt;td&gt;   M015&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Strip the first two characters off the DBNs so we can join to School Code&lt;/span&gt;
&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DBN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DBN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DBN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DBN&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;

&lt;span class="c1"&gt;#We can now see the keys match&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL CODE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div style="max-height:1000px;max-width:1500px;overflow:auto;"&gt;
&lt;table border="1" class="dataframe"&gt;
  &lt;thead&gt;
    &lt;tr style="text-align: right;"&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;0&lt;/th&gt;
      &lt;th&gt;1&lt;/th&gt;
      &lt;th&gt;2&lt;/th&gt;
      &lt;th&gt;3&lt;/th&gt;
      &lt;th&gt;4&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DBN&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
      &lt;td&gt; M019&lt;/td&gt;
      &lt;td&gt; M020&lt;/td&gt;
      &lt;td&gt; M034&lt;/td&gt;
      &lt;td&gt; M063&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DBN&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; M292&lt;/td&gt;
      &lt;td&gt; M448&lt;/td&gt;
      &lt;td&gt; M450&lt;/td&gt;
      &lt;td&gt; M458&lt;/td&gt;
      &lt;td&gt; M509&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;SCHOOL CODE&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
      &lt;td&gt; M015&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h2&gt;Primary Keys - Districts&lt;/h2&gt;
&lt;p&gt;We have a similar story with the district foreign keys. Again, we need to normalize the keys. The only additional complexity here is that dsProgReports['DISTRICT'] is typed numerically, whereas the other two district keys are typed as string. We do some type conversions following the key munging.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Show the key mismatchs&lt;/span&gt;
&lt;span class="c1"&gt;#For variety&amp;#39;s sake, using slicing ([:3]) syntax instead of .take()&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;District&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div style="max-height:1000px;max-width:1500px;overflow:auto;"&gt;
&lt;table border="1" class="dataframe"&gt;
  &lt;thead&gt;
    &lt;tr style="text-align: right;"&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;0&lt;/th&gt;
      &lt;th&gt;1&lt;/th&gt;
      &lt;th&gt;2&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DISTRICT&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;                1&lt;/td&gt;
      &lt;td&gt;                1&lt;/td&gt;
      &lt;td&gt;                1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;JURISDICTION NAME&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; CSD 01 Manhattan&lt;/td&gt;
      &lt;td&gt; CSD 02 Manhattan&lt;/td&gt;
      &lt;td&gt; CSD 03 Manhattan&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;District&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;      DISTRICT 01&lt;/td&gt;
      &lt;td&gt;      DISTRICT 02&lt;/td&gt;
      &lt;td&gt;      DISTRICT 03&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Extract well-formed district key values&lt;/span&gt;
&lt;span class="c1"&gt;#Note the astype(int) at the end of these lines to coerce the column to a numeric type&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;re&lt;/span&gt;
&lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;([A-Za-z]*\s)([0-9]*)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;District&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;District&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#We can now see the keys match&lt;/span&gt;
&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;District&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div style="max-height:1000px;max-width:1500px;overflow:auto;"&gt;
&lt;table border="1" class="dataframe"&gt;
  &lt;thead&gt;
    &lt;tr style="text-align: right;"&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;0&lt;/th&gt;
      &lt;th&gt;1&lt;/th&gt;
      &lt;th&gt;2&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;DISTRICT&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; 1&lt;/td&gt;
      &lt;td&gt; 1&lt;/td&gt;
      &lt;td&gt; 1&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;JURISDICTION NAME&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; 1&lt;/td&gt;
      &lt;td&gt; 2&lt;/td&gt;
      &lt;td&gt; 3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;District&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt; 1&lt;/td&gt;
      &lt;td&gt; 2&lt;/td&gt;
      &lt;td&gt; 3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;h2&gt;Additional Cleanup&lt;/h2&gt;
&lt;p&gt;At this point we could do the joins, but there is messiness in the data still. First let's reindex the DataFrames so the semantics come out a bit cleaner. Pandas indexing is beyond the scope of this tutorial but suffice it to say it makes these operations easier.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Reindexing&lt;/span&gt;
&lt;span class="n"&gt;dsProgReports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsDistrict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;JURISDICTION NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL CODE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;District&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DBN&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's take a look at one of our target variables. Right away we see the "s" value, which shouldn't be there.&lt;/p&gt;
&lt;p&gt;We'll filter out the rows without data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#We can see the bad value&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DBN
M292    391
M448    394
M450    418
M458    385
M509      s
Name: Critical Reading Mean
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we filter it out&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#We create a boolean vector mask. Open question as to whether this semantically ideal...&lt;/span&gt;
&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Number of Test Takers&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;s&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;#Cast fields to integers. Ideally we should not need to be this explicit.&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Number of Test Takers&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Number of Test Takers&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Mathematics Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Mathematics Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Writing Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Writing Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#We can see those values are gone&lt;/span&gt;
&lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;DBN
M292    391
M448    394
M450    418
M458    385
M515    314
Name: Critical Reading Mean
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Feature Construction&lt;/h2&gt;
&lt;p&gt;dsClassSize will be a many-to-one join with dsSATs because dsClassSize contains multiple entries per school. We need to summarize and build features from this data in order to get one row per school that will join neatly to dsSATs.&lt;/p&gt;
&lt;p&gt;Additionally, the data has an irregular format, consisting of a number of rows per school describing different class sizes, then a final row for that school which contains no data except for a number in the final column, SCHOOLWIDE PUPIL-TEACHER RATIO.&lt;/p&gt;
&lt;p&gt;We need to extract the SCHOOLWIDE PUPIL-TEACHER RATIO rows, at which point we'll have a regular format and can build features via aggregate functions. We'll also drop any features that can't be easily summarized or aggregated and likely have no bearing on the SAT scores (like School Name).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#The shape of the data&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;BORO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CSD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SCHOOL&lt;/span&gt; &lt;span class="n"&gt;NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROGRAM&lt;/span&gt; &lt;span class="n"&gt;TYPE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;CORE&lt;/span&gt; &lt;span class="n"&gt;SUBJECT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MS&lt;/span&gt; &lt;span class="n"&gt;CORE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="n"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="n"&gt;CORE&lt;/span&gt; &lt;span class="n"&gt;COURSE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MS&lt;/span&gt; &lt;span class="n"&gt;CORE&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="n"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;SERVICE&lt;/span&gt; &lt;span class="n"&gt;CATEGORY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="n"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;CLASSES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOTAL&lt;/span&gt; &lt;span class="n"&gt;REGISTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AVERAGE&lt;/span&gt; &lt;span class="n"&gt;CLASS&lt;/span&gt; &lt;span class="n"&gt;SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;SIZE&lt;/span&gt; &lt;span class="n"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;SMALLEST&lt;/span&gt; &lt;span class="n"&gt;CLASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SIZE&lt;/span&gt; &lt;span class="n"&gt;OF&lt;/span&gt; &lt;span class="n"&gt;LARGEST&lt;/span&gt; &lt;span class="n"&gt;CLASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATA&lt;/span&gt; &lt;span class="n"&gt;SOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;SCHOOLWIDE&lt;/span&gt; &lt;span class="n"&gt;PUPIL&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;TEACHER&lt;/span&gt; &lt;span class="n"&gt;RATIO&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="mi"&gt;015&lt;/span&gt; &lt;span class="n"&gt;ROBERTO&lt;/span&gt; &lt;span class="n"&gt;CLEMENTE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="n"&gt;GEN&lt;/span&gt; &lt;span class="n"&gt;ED&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="n"&gt;ATS&lt;/span&gt;
  &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="mi"&gt;015&lt;/span&gt; &lt;span class="n"&gt;ROBERTO&lt;/span&gt; &lt;span class="n"&gt;CLEMENTE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt; &lt;span class="n"&gt;CTT&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="mf"&gt;21.0&lt;/span&gt; &lt;span class="n"&gt;ATS&lt;/span&gt;
  &lt;span class="n"&gt;nan&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="mi"&gt;015&lt;/span&gt; &lt;span class="n"&gt;ROBERTO&lt;/span&gt; &lt;span class="n"&gt;CLEMENTE&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt; &lt;span class="n"&gt;nan&lt;/span&gt;
  &lt;span class="mf"&gt;8.9&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Extracting the Pupil-Teacher Ratio&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Take the column&lt;/span&gt;
&lt;span class="n"&gt;dsPupilTeacher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOLWIDE PUPIL-TEACHER RATIO&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;#And filter out blank rows&lt;/span&gt;
&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsPupilTeacher&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOLWIDE PUPIL-TEACHER RATIO&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsPupilTeacher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsPupilTeacher&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;#Then drop from the original dataset&lt;/span&gt;
&lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOLWIDE PUPIL-TEACHER RATIO&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#Drop non-numeric fields&lt;/span&gt;
&lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;BORO&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;CSD&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL NAME&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;GRADE &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PROGRAM TYPE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;\
&lt;span class="s1"&gt;&amp;#39;CORE SUBJECT (MS CORE and 9-12 ONLY)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;CORE COURSE (MS CORE and 9-12 ONLY)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;\
&lt;span class="s1"&gt;&amp;#39;SERVICE CATEGORY(K-9* ONLY)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DATA SOURCE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#Build features from dsClassSize&lt;/span&gt;
&lt;span class="c1"&gt;#In this case, we&amp;#39;ll take the max, min, and mean&lt;/span&gt;
&lt;span class="c1"&gt;#Semantically equivalent to select min(*), max(*), mean(*) from dsClassSize group by SCHOOL NAME&lt;/span&gt;
&lt;span class="c1"&gt;#Note that SCHOOL NAME is not referenced explicitly below because it is the index of the dataframe&lt;/span&gt;
&lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsClassSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
    &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;lsuffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.max&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
    &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;lsuffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.min&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rsuffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.mean&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
    &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsPupilTeacher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;array([NUMBER OF CLASSES.max, TOTAL REGISTER.max, AVERAGE CLASS SIZE.max,
       SIZE OF SMALLEST CLASS.max, SIZE OF LARGEST CLASS.max,
       NUMBER OF CLASSES.min, TOTAL REGISTER.min, AVERAGE CLASS SIZE.min,
       SIZE OF SMALLEST CLASS.min, SIZE OF LARGEST CLASS.min,
       NUMBER OF CLASSES.mean, TOTAL REGISTER.mean,
       AVERAGE CLASS SIZE.mean, SIZE OF SMALLEST CLASS.mean,
       SIZE OF LARGEST CLASS.mean, SCHOOLWIDE PUPIL-TEACHER RATIO], dtype=object)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Joining&lt;/h2&gt;
&lt;p&gt;One final thing before we join - dsProgReports contains distinct rows for separate grade level blocks within one school. For instance one school (one DBN) might have two rows: one for middle school and one for high school. We'll just drop everything that isn't high school.&lt;/p&gt;
&lt;p&gt;And finally we can join our data. Note these are inner joins, so district data get joined to each school in that district.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL LEVEL*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;High School&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dsProgReports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dsSATs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsClassSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsProgReports&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsDistrict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;\
&lt;span class="n"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsAttendEnroll&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTRICT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Additional Cleanup 2&lt;/h2&gt;
&lt;p&gt;We should be in a position to build a predictive model for our target variables right away but unfortunately there is still messy data floating around in the dataframe that machine learning algorithms will choke on. A pure feature matrix should have only numeric features, but we can see that isn't the case. However for many of these columns, the right approach is obvious once we've dug in.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;object&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;School Name                      object
SCHOOL                           object
PRINCIPAL                        object
PROGRESS REPORT TYPE             object
SCHOOL LEVEL*                    object
2009-2010 OVERALL GRADE          object
2009-2010 ENVIRONMENT GRADE      object
2009-2010 PERFORMANCE GRADE      object
2009-2010 PROGRESS GRADE         object
2008-09 PROGRESS REPORT GRADE    object
YTD % Attendance (Avg)           object
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;#Just drop string columns.&lt;/span&gt;
&lt;span class="c1"&gt;#In theory we could build features out of some of these, but it is impractical here&lt;/span&gt;
&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;School Name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PRINCIPAL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCHOOL LEVEL*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PROGRESS REPORT TYPE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#Remove % signs and convert to float&lt;/span&gt;
&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;YTD % Attendance (Avg)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;YTD % Attendance (Avg)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#The last few columns we still have to deal with&lt;/span&gt;
&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dtypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;object&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2009&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2010&lt;/span&gt; &lt;span class="n"&gt;OVERALL&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt;          &lt;span class="n"&gt;object&lt;/span&gt;
&lt;span class="mf"&gt;2009&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2010&lt;/span&gt; &lt;span class="n"&gt;ENVIRONMENT&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt;      &lt;span class="n"&gt;object&lt;/span&gt;
&lt;span class="mf"&gt;2009&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2010&lt;/span&gt; &lt;span class="n"&gt;PERFORMANCE&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt;      &lt;span class="n"&gt;object&lt;/span&gt;
&lt;span class="mf"&gt;2009&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2010&lt;/span&gt; &lt;span class="n"&gt;PROGRESS&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt;         &lt;span class="n"&gt;object&lt;/span&gt;
&lt;span class="mf"&gt;2008&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;09&lt;/span&gt; &lt;span class="n"&gt;PROGRESS&lt;/span&gt; &lt;span class="n"&gt;REPORT&lt;/span&gt; &lt;span class="n"&gt;GRADE&lt;/span&gt;    &lt;span class="n"&gt;object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Ordered Categorical Variables&lt;/h2&gt;
&lt;p&gt;We can see above that the remaining non-numeric field are grades . Intuitively, they might be important so we don't want to drop them, but in order to get a pure feature matrix we need numeric values. The approach we'll use here is to explode these into multiple boolean columns. Some machine learning libraries effectively do this for you under the covers, but when the cardinality of the categorical variable is relatively low, it's nice to be explicit about it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;gradeCols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 OVERALL GRADE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 ENVIRONMENT GRADE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 PERFORMANCE GRADE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 PROGRESS GRADE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2008-09 PROGRESS REPORT GRADE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;grades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gradeCols&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;#[nan, A, B, C, D, F]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;gradeCols&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grades&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;_is_&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gradeCols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;#Uncomment to generate csv files&lt;/span&gt;
&lt;span class="c1"&gt;#final.drop([&amp;#39;Critical Reading Mean&amp;#39;,&amp;#39;Mathematics Mean&amp;#39;,&amp;#39;Writing Mean&amp;#39;],axis=1).to_csv(&amp;#39;C:/data/NYC_Schools/train.csv&amp;#39;)&lt;/span&gt;
&lt;span class="c1"&gt;#final.filter([&amp;#39;Critical Reading Mean&amp;#39;,&amp;#39;Mathematics Mean&amp;#39;,&amp;#39;Writing Mean&amp;#39;]).to_csv(&amp;#39;C:/data/NYC_Schools/target.csv&amp;#39;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Modeling&lt;/h2&gt;
&lt;p&gt;We now have a feature matrix and it's trivial to use it with any number of machine learning algorithms.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestRegressor&lt;/span&gt;

&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="c1"&gt;#We drop all three dependent variables because we don&amp;#39;t want them used when trying to make a prediction.&lt;/span&gt;
&lt;span class="n"&gt;train&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Critical Reading Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Writing Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Mathematics Mean&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RandomForestRegressor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_jobs&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compute_importances&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;rmse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;imp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feature_importances_&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;tup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;RMSE: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rmse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;10 Most Important Variables:&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imp&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;RMSE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;80.13105688&lt;/span&gt;
&lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="n"&gt;Most&lt;/span&gt; &lt;span class="n"&gt;Important&lt;/span&gt; &lt;span class="n"&gt;Variables&lt;/span&gt;&lt;span class="o"&gt;:[(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PEER INDEX*&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.81424747874371173&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;TOTAL REGISTER.min&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.060086333792196724&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 ENVIRONMENT CATEGORY SCORE&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.023810405565050669&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 ADDITIONAL CREDIT&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.021788425210174274&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 OVERALL SCORE&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.019046860376900468&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AVERAGE CLASS SIZE.mean&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0094882658926829649&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 PROGRESS CATEGORY SCORE&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0094678349064146652&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AVERAGE CLASS SIZE.min&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0063723026953534942&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2009-2010 OVERALL GRADE_is_nan&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0057710237481254567&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Number of Test Takers&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0053660239780210584&lt;/span&gt;&lt;span class="o"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Entering Kaggle Competitions with Google Predict</title><link href="http://blog.untrod.com/2013/01/entering-kaggle-competitions-with.html" rel="alternate"></link><published>2013-01-17T16:30:00-08:00</published><updated>2013-01-17T16:30:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2013-01-17:/2013/01/entering-kaggle-competitions-with.html</id><summary type="html">&lt;p&gt;BigML had a &lt;a href="http://blog.bigml.com/2013/01/04/machine-learning-throwdown-the-reckoning/"&gt;great series of
posts&lt;/a&gt;
over the summer pitting some prediction-as-a-service products against
each other. One of those was the &lt;a href="https://developers.google.com/prediction/"&gt;Google Predict
API&lt;/a&gt;. I thought it might be
fun to enter a Kaggle competition using the API and see how it did
against some of the world's top …&lt;/p&gt;</summary><content type="html">&lt;p&gt;BigML had a &lt;a href="http://blog.bigml.com/2013/01/04/machine-learning-throwdown-the-reckoning/"&gt;great series of
posts&lt;/a&gt;
over the summer pitting some prediction-as-a-service products against
each other. One of those was the &lt;a href="https://developers.google.com/prediction/"&gt;Google Predict
API&lt;/a&gt;. I thought it might be
fun to enter a Kaggle competition using the API and see how it did
against some of the world's top data scientists.&lt;/p&gt;
&lt;p&gt;It turns out this was a terrible, terrible waste of time.&lt;/p&gt;
&lt;p&gt;If you are expecting a rigorous analysis of the Google Predict API, this
post will be a disappointment. In fact, I'll go so far as to ruin the
surprise right now: on the Biological Response competition, the Predict
API turned in a 0.67245 on the private leaderboard, just edging out the
optimized constant value benchmark (in a nutshell - it did badly). It
fared a bit better on the Titanic competition, scoring a
somewhat-reasonable 0.79426 (tied with 55 other users for 112th place as
of this writing).&lt;/p&gt;
&lt;p&gt;So instead of focusing on the actual performance of the algorithm, I
will instead share some tips and tricks for using the Google Predict
API:&lt;/p&gt;
&lt;h3&gt;Chris' Tips for Using the Google Predict API:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Turn off two factor authentication, unless your idea of a good time
    is spending a furious evening at home, battling OAuth. It is a
    formidable enemy.&lt;/li&gt;
&lt;li&gt;If you are predicting a binary response, be sure to replace those 0s
    and 1s in your response column with strings like TRUE and FALSE, or
    else Google will attempt to perform Magic (stand back!) and decide
    that your model is actually a regression model and will cheerfully
    return values like -0.8 and 1.2.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Nice work - now your response column is now filled with TRUE and
    FALSE strings and you have persuaded Google that you want a
    categorical response. You also remembered to remove that header row,
    right? Oh...you didn't? Well then get ready for the excitement of
    responses like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s2"&gt;&amp;quot;outputMulti&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;label&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;TRUE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;score&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2001&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;label&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;FALSE&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;score&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7829&lt;/span&gt;
   &lt;span class="p"&gt;},&lt;/span&gt;
   &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;label&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;HEADER_TEXT&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="s2"&gt;&amp;quot;score&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.017&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
 &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;So yeah, remove those headers. And on a related note, have fun
    writing your own custom parsing code to extract the most likely
    label from this unsorted list. Because we certainly wouldn't want to
    pollute the integrity of the response schema with something as handy
    as providing the predicted label as a first class property.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;The thing you are creating is a TrainedModel, &lt;em&gt;not&lt;/em&gt; a HostedModel.
    Just trust me on this - they may have identical predict methods, and
    be used interchangeably in example code, but you are making
    TrainedModels and will get errors if you ever accidentally refer
    to HostedModels.&lt;/li&gt;
&lt;li&gt;If you are using &lt;a href="http://code.google.com/p/google-api-python-client/"&gt;Google's Python API
    wrappers&lt;/a&gt;, be
    sure you are constructing your feature array like this:
    &lt;code&gt;["foo","bar",1.23\]&lt;/code&gt;
    and &lt;em&gt;not&lt;/em&gt; like this:
    &lt;code&gt;["foo","bar","1.23"\]&lt;/code&gt;. I made the mistake of reading in my
    training CSV file as an array of strings, which caused my requests
    to look like the latter. The result?  The Predict API merrily
    chirps back with a valid, but completely terrible, prediction.&lt;/li&gt;
&lt;li&gt;Get a cup of coffee. Google gets angry if you make too many
    prediction requests in a row and starts throwing errors. So you have
    to resort to weird throttling tricks if you need more than a few
    hundred predictions at once. And bulk prediction is not available.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My terrible, horrible code that does this &lt;a href="https://github.com/chrisclark/GooglePredictForKaggle"&gt;is on
github&lt;/a&gt;. It's not
a particularly usable form and does some...questionable things because I
didn't learn some of these lessons until it was too late. But if you are
absolutely determined to use the Google Predict API (god help you), it
might get you started.&lt;/p&gt;
&lt;p&gt;Also, kudos to the BigML engineers who are either way smarter than me
and got this working right away, or have a remarkable amount of
self-restraint and did not complain one jot.&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Extensible, Single-Line Fizzbuzz in a Tweet</title><link href="http://blog.untrod.com/2012/11/extensible-single-line-fizzbuzz-in-tweet.html" rel="alternate"></link><published>2012-11-01T19:55:00-07:00</published><updated>2012-11-01T19:55:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-11-01:/2012/11/extensible-single-line-fizzbuzz-in-tweet.html</id><summary type="html">&lt;p&gt;Yep, it's a
&lt;a href="http://www.codinghorror.com/blog/2007/02/why-cant-programmers-program.html"&gt;fizzbuzz&lt;/a&gt;
blog post! I can hardly believe I've gone this long without ever doing
one.&lt;/p&gt;
&lt;p&gt;I was thinking about hiring and what I would do if someone asked me
fizzbuzz, and I think I would have used it as an opportunity to show off
some engineering practices …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Yep, it's a
&lt;a href="http://www.codinghorror.com/blog/2007/02/why-cant-programmers-program.html"&gt;fizzbuzz&lt;/a&gt;
blog post! I can hardly believe I've gone this long without ever doing
one.&lt;/p&gt;
&lt;p&gt;I was thinking about hiring and what I would do if someone asked me
fizzbuzz, and I think I would have used it as an opportunity to show off
some engineering practices. Basically, I'd write unit tests and make it
extensible. Before I knew it this was no longer a thought exercise and I
was fiddling around in IPython Notebook (which is great for this sort of
thing) and created very explicit implementation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;swaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I like this approach because it's reasonably extensible - just add more
tuples to the list of swaps and everything works. Want to print "bazz"
for multiples of 4? Just add swaps.append((4, "bazz")).&lt;/p&gt;
&lt;p&gt;But it's pretty boring. As I'm always trying to get better at writing
idiomatic Python, I started swapping in some more Pythonic constructs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;swaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;xrange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;swaps&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now of course, I'm hooked and decide I want to shrink it down to fit in
a Tweet. Not terribly difficult, just rename variables, eliminate
spaces, and condense variable declarations:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And I still have 21 characters to spare. But Twitter doesn't support
linebreaks so if I were to Tweet it, no one could run it without
guessing at the indentation and linebreaks. The semicolon trick almost
works but only gets me this far (the line break after the semicolon is
still required):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is because Python won't let you inline control flow statements. I
don't want to move the variable declaration inline because it
contradicts any semblance of extensibility still here (I know this is
silly, but ya gotta have yer principles), so I have to get creative.&lt;/p&gt;
&lt;p&gt;Instead of a loop, a construct like &lt;code&gt;map(lambda, range(1,101))&lt;/code&gt;  will
work. Again though, a  little creativity is in order because
"print" is a statement and can't appear inside a lambda (&lt;a href="http://docs.python.org/2/reference/expressions.html#lambda"&gt;only
expressions&lt;/a&gt;
are allowed). Instead I can use &lt;code&gt;sys.stdout.write&lt;/code&gt;. Finally, I added a
neat little trick to avoid the if/else (multiplying a string by a
boolean value) and wound up with this functioning, extensible, single
line Fizzbuzz that fits in a Tweet:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sure there are &lt;a href="http://stackoverflow.com/a/6890045/221390"&gt;shorter
versions&lt;/a&gt; out there, but I
had a lot of fun with my version and picked up a couple additional
Python tricks on the way.&lt;/p&gt;
&lt;p&gt;(for completeness, if I caved and inlined the variable declaration, I
get this, which is pretty darn short)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;101&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fizz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;),(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;buzz&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Rapidly Saving .jpgs in Photoshop</title><link href="http://blog.untrod.com/2012/10/rapidly-saving-jpgs-in-photoshop.html" rel="alternate"></link><published>2012-10-17T23:52:00-07:00</published><updated>2012-10-17T23:52:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-10-17:/2012/10/rapidly-saving-jpgs-in-photoshop.html</id><summary type="html">&lt;p&gt;&lt;strong&gt;Cliffs notes&lt;/strong&gt;: Now, whenever I hit F2 in Photoshop, I get a high
quality jpg of my file saved to the same directory. No more
"File-&amp;gt;Save As" nonsense every time I want a static version of the
image.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What/how/why:&lt;/strong&gt; I was working on some product mock-ups this …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Cliffs notes&lt;/strong&gt;: Now, whenever I hit F2 in Photoshop, I get a high
quality jpg of my file saved to the same directory. No more
"File-&amp;gt;Save As" nonsense every time I want a static version of the
image.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What/how/why:&lt;/strong&gt; I was working on some product mock-ups this morning in
Photoshop using layers to show and hide different bits of the mockup,
depending on the state of the application. It was time to wire all of
the various states together in a clickable mock-up (I use
&lt;a href="http://invisionapp.com/"&gt;invisionapp.com&lt;/a&gt; to do this - highly
recommended) so I needed to save off a whole bunch of images from
Photoshop, with different layers set to visible. I quickly tired of
going to File-&amp;gt;Save As-&amp;gt;Select Jpg file type-&amp;gt;[Type file
name]-&amp;gt;Hit save. There has to be a better way!&lt;/p&gt;
&lt;p&gt;After Googling some options (I am not a PS expert), I decided to create
a script that would save the image as a jpg, with a unique name
generated from a timestamp. I found some sample code for the various
pieces and came up with the script below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;//Cobbled together from:&lt;/span&gt;
&lt;span class="c1"&gt;//http://feedback.photoshop.com/photoshop_family/topics/quick_save_as&lt;/span&gt;
&lt;span class="c1"&gt;//http://www.polycount.com/forum/showthread.php?t=85425&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="nx"&gt;photoshop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;thedoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeDocument&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;docName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thedoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;basename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/(.*)\.[^\.]+$/&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;basename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;//getting the location, if unsaved save to desktop;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;docPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thedoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;docPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;~/Desktop&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;jpegOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;JPEGSaveOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;jpegOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;jpegOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedColorProfile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;jpegOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matte&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;MatteType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;docPath&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;basename&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;.jpg&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;thedoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;saveAs&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="nx"&gt;jpegOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;//Make single-digit mins show up as 6:01 and not 6:1&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;0&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;timeStamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;-&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHours&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;minutes&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;
  &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMilliseconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;timeStamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you save this as "Quick save to jpg.jsx" to &lt;code&gt;\\Program
Files\\Adobe\\Adobe Photoshop CS5.1\\Presets\\Scripts&lt;/code&gt; (or
&lt;code&gt;/Applications/Adobe Photoshop CS6/Presets/Scripts&lt;/code&gt; on Mac OS X), you
will then be able to access the script by going to File-&amp;gt;Scripts. I
went one step further and recorded an action of the script firing and
bound it to my &lt;code&gt;f2&lt;/code&gt; key.&lt;/p&gt;
&lt;p&gt;Now, whenever I'm in Photoshop, I can just hit &lt;code&gt;f2&lt;/code&gt; and get a .jpg image
saved to my current directory, with no dialogs or anything! Neat!&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Engineering Practices in Data Science</title><link href="http://blog.untrod.com/2012/10/engineering-practices-in-data-science.html" rel="alternate"></link><published>2012-10-04T17:28:00-07:00</published><updated>2012-10-04T17:28:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-10-04:/2012/10/engineering-practices-in-data-science.html</id><summary type="html">&lt;p&gt;&lt;a href="http://twitter.com/jasonbaldridge/statuses/198115689175859201"&gt;Josh
Wills&lt;/a&gt;
wrote this excellent, pithy definition of a data scientist:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Data Scientists (n.): Person who is better at statistics than any
software engineer and better at software engineering than any
statistician.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's certainly true that software engineering and data science are two
different disciplines, and for good reason - they …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="http://twitter.com/jasonbaldridge/statuses/198115689175859201"&gt;Josh
Wills&lt;/a&gt;
wrote this excellent, pithy definition of a data scientist:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Data Scientists (n.): Person who is better at statistics than any
software engineer and better at software engineering than any
statistician.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's certainly true that software engineering and data science are two
different disciplines, and for good reason - they require different
skills. But as this definition points out, in the same way an artist and
an interior painter might share the medium of paint and thus a set of
best practices (invest in canvas drop cloths, write down color choices),
so do data scientists and software engineers share the medium of code.&lt;/p&gt;
&lt;p&gt;Among the &lt;a href="http://www.kaggle.com/"&gt;Kaggle&lt;/a&gt; community there seem to be a
few consistent ways that engineering practices intersect with data
science. These trends are thrown into particularly sharp relief by the
competition format we use at Kaggle, but are present to varying extents
in data science teams across industry.&lt;/p&gt;
&lt;p&gt;To start, it terrifies me that....&lt;/p&gt;
&lt;h3&gt;...many data scientists don't use source control.&lt;/h3&gt;
&lt;p&gt;To those who breathe Git and dream in command line interfaces, it might
be hard to believe that many data scientists (smart ones! great ones!)
just plain don't use version control. If you ask them they might say
something like "Sure I use version control! I email myself every new
version of the R script. Plus it's on dropbox."&lt;/p&gt;
&lt;p&gt;I can't back it up with hard data, but it seems data scientists who use
R, Matlab, SAS, or other statistical programming languages use source
control less frequently than people who use Python or Java. And this
sort of makes sense - the stats community is often more academically
oriented and not as steeped in engineering practices as the Python
community, for instance.&lt;/p&gt;
&lt;p&gt;And frankly - who can blame data scientists for not using source
control? The de facto choice, Git, might be free but is totally obtuse
(though R isn't exactly a haven of good design...) and most of the time
saving files to disk with clever names seems to work OK. For data
scientists, source control on its own just isn't valuable enough to
bother with. Contrast this with software engineering: source control
isn't just source control but the jumping off point for continuous
integration builds, deployments, and code reviews. For data scientists
to use source control (and they should - it makes collaboration easier,
and mistakes less likely), it has to be more valuable. And I'm
optimistic that we will get there as...&lt;/p&gt;
&lt;h3&gt;....successful data scientists learn the value of good pipeline engineering.&lt;/h3&gt;
&lt;p&gt;A well-engineered pipeline gets data scientists iterating much faster,
which can be a big competitive edge in a Kaggle competition. This is
especially true for datasets that require a lot of feature engineering -
a crisp pipeline with well defined phases (data ingestion -&amp;gt; feature
extraction -&amp;gt; training -&amp;gt; ensembling -&amp;gt; validation) allows
disciplined data scientists to try out a lot more ideas than someone
with a pile of spaghetti Python code.&lt;/p&gt;
&lt;p&gt;I expect we'll soon start to see more open source data science pipelines
that couple nicely with popular machine learning libraries and tools.
The more tooling and community support we have for good pipelines, the
less we'll see...&lt;/p&gt;
&lt;h3&gt;...good engineering going out the window quickly with elaborate ensembling.&lt;/h3&gt;
&lt;p&gt;This effect is probably more specific to Kaggle than the others but it's
fun and instructive, so I'll write about it anyway.&lt;/p&gt;
&lt;p&gt;In the final hours of a competition we sometimes see unholy ensembles of
different languages and techniques being mashed together for a final
push. It's something we try to discourage by encouraging team formation
early on (so a nice pipeline gets built), but it does occasionally
happen. In one particularly gruesome case we saw some Java code spawning
processes which ran command line shells in order to launch the R
executable while the Java program piped data and commands back and forth
between the shells. Yikes!&lt;/p&gt;
&lt;h3&gt;In Closing...&lt;/h3&gt;
&lt;p&gt;Data scientists are slowly but surely adopting engineering practices
that enable them to get the goo of software out of the way so they can
focus on valuable data problems. I hope that we'll soon start to see
well engineered data pipelines emerge as open source projects that
generalize well over particular classes of problems, complement existing
data science libraries, and encourage modular development and use of
source control.&lt;/p&gt;
&lt;p&gt;Or someone writes a killer general-purpose deep learning library and we
all just use that.&lt;/p&gt;</content><category term="Data Science"></category></entry><entry><title>Getting Started With Python for Data Science</title><link href="http://blog.untrod.com/2012/07/getting-started-with-python-for-data.html" rel="alternate"></link><published>2012-07-20T22:17:00-07:00</published><updated>2012-07-20T22:17:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-07-20:/2012/07/getting-started-with-python-for-data.html</id><summary type="html">&lt;p&gt;As the product manager at &lt;a href="http://www.kaggle.com/"&gt;Kaggle&lt;/a&gt;
I'm fortunate enough to hang around with some of the world's top machine
learning experts. And working at a place that specializes in running
predictive modelling competitions means that I inevitably got the itch
to learn some of this stuff myself.&lt;/p&gt;
&lt;p&gt;I don't have …&lt;/p&gt;</summary><content type="html">&lt;p&gt;As the product manager at &lt;a href="http://www.kaggle.com/"&gt;Kaggle&lt;/a&gt;
I'm fortunate enough to hang around with some of the world's top machine
learning experts. And working at a place that specializes in running
predictive modelling competitions means that I inevitably got the itch
to learn some of this stuff myself.&lt;/p&gt;
&lt;p&gt;I don't have a super-strong math or machine learning background, but I
figured I'm a decent-enough programmer and have all these smart people
around me, so I figured I should be able to figure out how to use some
off-the-shelf tools to build some predictive models of my own.&lt;/p&gt;
&lt;p&gt;I wrote the following tutorial after my experience hacking away on a
Kaggle competition for a weekend. I hope it serves to get other
programming-literate but machine learning-ignorant people like me up and
running, building their own models, and learning data science. So
without further ado...&lt;/p&gt;
&lt;h2&gt;Who is this for and what will I learn?&lt;/h2&gt;
&lt;p&gt;(Cross posted on Kaggle's blog, &lt;a href="http://blog.kaggle.com/"&gt;No Free Hunch&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;This tutorial assumes some knowledge of Python and programming, but no
knowledge whatsoever of data science, machine learning, or predictive
modeling (or, heck, even statistics). To the extent there is a target
audience, it's probably hacker types who learn best by doing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All the code from this tutorial is available
on &lt;a href="https://github.com/chrisclark/PythonForDataScience"&gt;github&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You might encounter terms you're not familiar with, but that shouldn't
stop you from completing the tutorial. By the end, you won't know a heck
of a lot more about data science &lt;em&gt;per se&lt;/em&gt;, but you'll have a nice
environment set up where you can easily play with lots of different data
science tools and even make credible entries to Kaggle competitions.
Most importantly you'll be in a great position to experiment and learn
more data science.&lt;/p&gt;
&lt;p&gt;Here's what you'll learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How to install popular scientific and statistical computing
    libraries for Python&lt;/li&gt;
&lt;li&gt;Use those libraries to create a benchmark model and submit it to
    a competition.&lt;/li&gt;
&lt;li&gt;Write your own evaluation function, and learn how to use
    cross-validation to
   test out ideas on your own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Excited? I thought so!&lt;/p&gt;
&lt;h2&gt;1. Environment Setup&lt;/h2&gt;
&lt;p&gt;First thing, we'll need a Python environment suitable for scientific and
statistical computing. Assuming you already have Python installed (no?
Well then &lt;a href="http://www.python.org/getit/"&gt;get it&lt;/a&gt;! Python 2.7 is
recommended), we'll need three packages. You should install each in the
order they appear here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="http://sourceforge.net/projects/numpy/files/"&gt;numpy&lt;/a&gt; -
    (pronounced &lt;em&gt;num-pie&lt;/em&gt;) Powerful numerical arrays. A foundational
    package
   for the two packages below.&lt;/li&gt;
&lt;li&gt;&lt;a href="http://sourceforge.net/projects/scipy/files/"&gt;scipy&lt;/a&gt; - (&lt;em&gt;sigh-pie&lt;/em&gt;)
    Scientific, mathematical, and engineering package&lt;/li&gt;
&lt;li&gt;&lt;a href="http://scikit-learn.org/stable/install.html"&gt;scikit-learn&lt;/a&gt; - Easy
    to use machine learning library&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: 64 bit versions of these libraries can be
found &lt;a href="http://www.lfd.uci.edu/~gohlke/pythonlibs/"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Click through the links above for the home pages of each project and get
the installation for your operating system or, if you're running Linux,
you can install from a package manager (pip). If you're on a Windows
machine, it's easiest to install using the setup executables for scipy
and scikit-learn rather than installing from a package manager.&lt;/p&gt;
&lt;p&gt;I'd also highly recommend to setting up a decent Python development
environment. You can certainly execute Python scripts from the command
line, but it's a heck of a lot easier to use a proper environment with
debugging support. I use &lt;a href="http://pydev.org/"&gt;PyDev&lt;/a&gt;, but even something
like IPython is better than nothing.&lt;/p&gt;
&lt;p&gt;Now you're ready for machine learning greatness!&lt;/p&gt;
&lt;h2&gt;2. Your First Submission&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://www.kaggle.com/c/bioresponse"&gt;Biological
Response&lt;/a&gt; competition provides a
great data set to get started with because the value to be predicted is
a simple binary classifier (0 or 1) and the data is just a bunch of
numbers, so
feature &lt;a href="http://en.wikipedia.org/wiki/Feature_extraction"&gt;extraction&lt;/a&gt; and &lt;a href="http://en.wikipedia.org/wiki/Feature_selection"&gt;selection&lt;/a&gt; aren't
as important as in some other Kaggle
competitions. &lt;a href="https://www.kaggle.com/c/bioresponse/data"&gt;Download&lt;/a&gt; the
training and test data sets now. Even though this competition is over,
you can still make submissions and see how you compare to the world's
best data scientists.&lt;/p&gt;
&lt;p&gt;In the code below, we'll use an ensemble classifier called a &lt;a href="http://en.wikipedia.org/wiki/Random_forest"&gt;random
forest&lt;/a&gt; that often performs
very well as-is, without much babysitting and parameter-tweaking.
Although a random forest is actually a pretty sophisticated classifier,
it's a piece of cake to get up and running with one thanks to sklearn.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Remember: You don't have to understand all of the underlying
mathematics to use these techniques. Experimentation is a great way to
start getting a feel for how this stuff works. Understanding the models
is important, but it's not necessary to get started, have fun, and
compete.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here's the code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;genfromtxt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;savetxt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;#create the training &amp;amp; test sets, skipping the header row with [1:]&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genfromtxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Data/train.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;f8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;train&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genfromtxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Data/test.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;f8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

    &lt;span class="c1"&gt;#create and train the random forest&lt;/span&gt;
    &lt;span class="c1"&gt;#multi-core CPUs can use: rf = RandomForestClassifier(n_estimators=100, n_jobs=2)&lt;/span&gt;
    &lt;span class="n"&gt;rf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;predicted_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="n"&gt;savetxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Data/submission.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicted_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At this point you should go ahead and &lt;em&gt;actually get this running&lt;/em&gt; by
plopping the into a new Python script and saving it as
makeSubmission.py. You should now have directories and files on your
computer like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;quot;My Kaggle Folder&amp;quot;)
 |
 |---&amp;quot;Data&amp;quot;
 |   |
 |   |---train.csv
 |   |
 |   |---test.csv
 |
 |---makeSubmission.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once you've run makeSubmission, you'll also have
&lt;code&gt;my_first_submission.csv&lt;/code&gt; in your data folder. Now it is time to do
something very important:&lt;/p&gt;
&lt;h3&gt;Submit this file to the bio-response competition&lt;/h3&gt;
&lt;p&gt;Did you do it? You did it! Great! You could stop here - you know how to
make a successful Kaggle entry - but if you're the curious type, I'm
sure you'll want to play around with other types of models, different
parameters, and other shiny things. Keep reading to learn an important
technique that will let you test models locally, without burning through
your daily Kaggle submission limit.&lt;/p&gt;
&lt;h2&gt;3. Evaluation and Cross-Validation&lt;/h2&gt;
&lt;p&gt;Let's say we wanted to try out sklearn's &lt;a href="http://scikit-learn.org/stable/modules/ensemble.html#gradient-tree-boosting"&gt;gradient boosting
machine&lt;/a&gt; instead
of a random forest. Or maybe some simple &lt;a href="http://scikit-learn.org/stable/modules/linear_model.html"&gt;linear
models&lt;/a&gt;. It's
easy enough to important these things from sklearn and generate
submission files, but it's not so easy to compare their performance.
It's not practical to upload a new submission every time we make a
change to the model - we'll need a way to test things out locally, and
we'll need two things in order to do that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An evaluation function&lt;/li&gt;
&lt;li&gt;Cross validation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You'll always need some kind of evaluation function to determine how
your models are performing. Ideally, these would be identical to the
evaluation metric that Kaggle is using to score your entry. Competition
participants often post evaluation code in the forums, and Kaggle has
detailed descriptions of the metrics available on
the &lt;a href="https://www.kaggle.com/wiki/Metrics"&gt;wiki&lt;/a&gt;. In the case of the
bio-response competition, the evaluation metric
is &lt;a href="https://www.kaggle.com/wiki/LogarithmicLoss"&gt;log-loss&lt;/a&gt; and user
Grunthus
has &lt;a href="https://www.kaggle.com/c/bioresponse/forums/t/1831/python-code-for-log-loss"&gt;posted&lt;/a&gt; a
Python version of it. We won't spend too much time on this (read the
forum post for more information), but go ahead and save the following
into your working directory as logloss.py.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;scipy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;sp&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;llfun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;epsilon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1e-15&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maximum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epsilon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;epsilon&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="n"&gt;ll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ll&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;act&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ll&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, we'll need data to test our models against. When you submitted
your first Kaggle competition entry earlier in this tutorial, Kaggle
compared (using log-loss) your answers to the actual real world results
(the "ground truth") associated with the test data set. Without access
to those answers, how can we actually test our models locally?
Cross-validation to the rescue!&lt;/p&gt;
&lt;p&gt;Cross-validation is a simple technique that basically grabs a chunk of
the training data and holds it in reserve while the model is trained on
the remainder of the data set. In case you haven't realized it yet,
sklearn it totally awesome and is here to help. It has built in tools to
generate cross validation sets. The
sklearn &lt;a href="http://scikit-learn.org/0.10/modules/cross_validation.html"&gt;documentation&lt;/a&gt; has
a lot of great information on cross-validation.&lt;/p&gt;
&lt;p&gt;The code below creates 10 cross-validation sets (called &lt;em&gt;folds&lt;/em&gt;), each
with 10% of the training data set held in reserve, and tests our random
forest model against that withheld data.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;cross_validation&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logloss&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;#read in  data, parse into training and target sets&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;genfromtxt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Data/train.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;r&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;delimiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;f8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;train&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;#In this case we&amp;#39;ll use a random forest, but this could be any classifier&lt;/span&gt;
    &lt;span class="n"&gt;cfr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RandomForestClassifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;#Simple K-Fold cross validation. 5 folds.&lt;/span&gt;
    &lt;span class="n"&gt;cv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cross_validation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KFold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;#iterate through the training and test cross validation segments and&lt;/span&gt;
    &lt;span class="c1"&gt;#run the classifier on each one, aggregating the results into a list&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;traincv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testcv&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;probas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cfr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;traincv&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;traincv&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_proba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;testcv&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;logloss&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llfun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;testcv&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;probas&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;#print out the mean of the cross-validated results&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Results: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note that your cross-validated results might not exactly match the score
Kaggle gives you on the model. This could be for a variety of
(legitimate) reasons: random forests have a random component and won't
yield identical results every time; the actual test set might deviate
from the training set (especially when the sample size is fairly low,
like in the bio-response competition); the evaluation implementation
might differ slightly. Even if you are getting slightly different
results, you can compare model performance locally and you should know
when you have made an improvement.&lt;/p&gt;
&lt;h2&gt;That's a wrap!&lt;/h2&gt;
&lt;p&gt;That's it! We're done! You now have great tools at your disposal, and I
expect to see you at the top of the leaderboard in no time!&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Tale of the Tape (The Indentation Apocalypse)</title><link href="http://blog.untrod.com/2012/07/tale-of-tape-whitespace.html" rel="alternate"></link><published>2012-07-01T22:13:00-07:00</published><updated>2012-07-01T22:13:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-07-01:/2012/07/tale-of-tape-whitespace.html</id><summary type="html">&lt;p&gt;&lt;em&gt;BRUCE "Emacs" BUFFER:&lt;/em&gt;: Now entering the Kill Ring in the blue
trunks, with a reach of approximately three-quarters of an inch,
hailing from the 9th spot in the ASCII table, it's TAAAABBBBBB!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;BRUCE "Emacs" BUFFER&lt;/em&gt;: In the opposite corner, wearing white, you know him
well, don't-call-him-32-he-prefers-hexidecimal-number-20...it's
SPAAACCEEEEEE!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;COMMENTATOR 1 …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;BRUCE "Emacs" BUFFER:&lt;/em&gt;: Now entering the Kill Ring in the blue
trunks, with a reach of approximately three-quarters of an inch,
hailing from the 9th spot in the ASCII table, it's TAAAABBBBBB!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;BRUCE "Emacs" BUFFER&lt;/em&gt;: In the opposite corner, wearing white, you know him
well, don't-call-him-32-he-prefers-hexidecimal-number-20...it's
SPAAACCEEEEEE!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;COMMENTATOR 1&lt;/em&gt;: Well folks, have we got a treat for you tonight! They're
calling it the Indentation Apocalypse. These are two guys - they've
fought before - and let me tell you - there is no love lost between
these two.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;COMMENTATOR 2&lt;/em&gt;: You said it. While these guys are ostensibly fighting for
the title, you know their real focus is just on beating the other guy.
This one's personal, and should be a heck of a fight.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: It almost blew up at the pre-fight conference. Tab got right in the
face of Space, and accused Space of being semantically inferior.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: Oof, brutal stuff. But semantic arguments don't mean much in the
ring.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;REFEREE&lt;/em&gt;: (shouting, to be heard over the crowd): Let's have a good, clean
fight, keep everything above the ¶.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: And we're off!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: Listen to the crowd as Tab struts his stuff around the ring! He's
looking great as he sizes up Space - he really worked on his
configurability for this one. Such an innovator.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: You know, that's a good point. As a fighter, he's a real chameleon -
you ask one fan, he'll tell you he's 3/4", but another will tell you
he's a full 1.5". I guess that's a reason he's so popular with the
crowd.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: Yeah, but Space is so consistent and OH! He lands four crisp jabs to
Tab's left margin.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: Four jabs from Space is about as good as one big right from Tab.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: That's true, but Tab is so quick to recover after one of
those colossal right hands, he just takes one smooth step back, and he's
out of range. Space's footwork is a mess after those jabs - sometimes it
seems like he's gotta take a step back for every punch he throws!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: I agree. The efficiency edge definitely goes to Tab. At a
 quarter of the weight as well, there's just no question.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: But year after year, space just delivers the same performance.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: And he's versatile - remember, he's had a lot of international
success, and in some of smaller leagues, which have slightly different
rules. His techniques really seem to work equally well everywhere.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: In contrast, I'm sure many fans remember Tab's embarrassing Olympic
boxing trials. His inconsistencies really hurt him. The coach got so
frustrated trying to train him alongside the other team members, he
eventually had to kick him out of the program.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: Sometimes it's best to stick with the basics.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;...rounds pass...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: Neither fighter has been able to land the punch they're looking for.
The crowd's frustrated - they came here tonight to see a knockout.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C2&lt;/em&gt;: Am I glad I'm not a judge tonight. The scores are even on the cards.
If it goes to a decision, it's going to come down to this last round.
This crowd won't be happy whichever way it goes.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;C1&lt;/em&gt;: Ringside is already buzzing with talks of a rematch. This fight may
be almost over, but it's a rivalry for the ages, and we haven't seen the
last of these two.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Four Fun Facts From Big Data</title><link href="http://blog.untrod.com/2012/06/four-fun-facts-from-big-data.html" rel="alternate"></link><published>2012-06-29T03:00:00-07:00</published><updated>2012-06-29T03:00:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-06-29:/2012/06/four-fun-facts-from-big-data.html</id><summary type="html">&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The credit card industry's term of art for false positives in fraud
detection is "insult rate". If you've ever swiped your card at a shop
and had it declined, you understand why.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A large auto insurance company has discovered that if a household
owns two identical cars (same make, same …&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The credit card industry's term of art for false positives in fraud
detection is "insult rate". If you've ever swiped your card at a shop
and had it declined, you understand why.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A large auto insurance company has discovered that if a household
owns two identical cars (same make, same model), that household's
driving risk is very low. They have termed this "the boring factor".&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cable companies have trouble figuring out whether you are actually
watching TV - the box might be on, but the TV is off. But by inspecting
remote control button presses and using predictive models, they can
figure it out. If you hit the volume button, for instance, you're almost
certainly watching. And yes, that means every button press is probably
being logged.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Look for paginated articles on news websites to start disappearing.
Paginating articles is a way for content providers to "manufacture
capacity" (ie. create pageviews against which they can sell
advertising). But new display advertising technologies only register an
ad impression when the ad is actually viewed (so ads at the bottom of
the page aren't counted if the user never scrolls them into view). With
the incentive for pagination gone, look for more long, scrolling
articles.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;</content><category term="Data Science"></category></entry><entry><title>A Very Painful Bug</title><link href="http://blog.untrod.com/2012/04/a-very-painful-bug.html" rel="alternate"></link><published>2012-04-01T18:24:00-07:00</published><updated>2012-04-01T18:24:00-07:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-04-01:/2012/04/a-very-painful-bug.html</id><summary type="html">&lt;p&gt;I'll lead off this post by listing all of the various things I thought
might have caused this bug, and related phrases that I Googled in the
hopes that it will lead some poor soul chasing the same issue to this
post, thus shaving 2-3 points off that person's long-term …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'll lead off this post by listing all of the various things I thought
might have caused this bug, and related phrases that I Googled in the
hopes that it will lead some poor soul chasing the same issue to this
post, thus shaving 2-3 points off that person's long-term systolic blood
pressure.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Facebook authentication mobile safari crash&lt;/li&gt;
&lt;li&gt;jQuery mobile Facebook crash mobile safari&lt;/li&gt;
&lt;li&gt;Mobile safari crash switching windows&lt;/li&gt;
&lt;li&gt;mobile firebug&lt;/li&gt;
&lt;li&gt;Mobile safari ios crash logs&lt;/li&gt;
&lt;li&gt;Understanding ios crash logs&lt;/li&gt;
&lt;li&gt;ios crash webthread javascript&lt;/li&gt;
&lt;li&gt;juggernaut crash mobile safari&lt;/li&gt;
&lt;li&gt;socket.io mobile safari crash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Basically, there is a problem with how Safari handles websocket
connections. It manifests clearly when, on a mobile device, a page
spawns a child window for something like Facebook authentication (or any
Oauth flow). When the browser returns to the parent window after
authenticating, Safari will completely crash - or, at least, most of the
time it will completely crash. In this case there was a frustrating
dependency that made the bug not 100% reproducible.&lt;/p&gt;
&lt;p&gt;After hours of debugging (you know you're getting desperate when you
resort to binary-searching for the bug by systematically commenting out
half your JavaScript...) and Googling, I found &lt;a href="https://github.com/LearnBoost/socket.io/issues/193#issuecomment-4177697"&gt;this comment on
github&lt;/a&gt;
which describes the symptoms perfectly.&lt;/p&gt;
&lt;p&gt;I was experiencing it specifically with a connection spun up by
&lt;a href="https://github.com/maccman/juggernaut"&gt;Juggernaut&lt;/a&gt;, which is based on
&lt;a href="https://github.com/learnboost/socket.io"&gt;socket.io&lt;/a&gt;. The solution in my
case was simple - just prior to making the FB.login() authentication
call, I explicitly close Juggernaut's websocket and unsubscribe the
client from my PubSub channels:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;jug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;jug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unsubscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myChannel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then reconnect everything when I return from the Facebook auth
call:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;jug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;jug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;myChannel&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callBackFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Works like a charm, though good god it was painful to identify.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>Juggernaut in Windows</title><link href="http://blog.untrod.com/2012/03/juggernaut-in-windows.html" rel="alternate"></link><published>2012-03-09T19:49:00-08:00</published><updated>2012-03-09T19:49:00-08:00</updated><author><name>Chris</name></author><id>tag:blog.untrod.com,2012-03-09:/2012/03/juggernaut-in-windows.html</id><summary type="html">&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an old post, and no longer relevant as Juggernaut is
no longer a thing. Just use
&lt;a href="https://github.com/socketio/socket.io/"&gt;socket.io&lt;/a&gt; directly.&lt;/p&gt;
&lt;p&gt;I've fallen in love with
&lt;a href="https://github.com/maccman/juggernaut"&gt;Juggernaut&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Juggernaut gives you a realtime connection between your servers and
client browsers. You can literally push data to clients using your web …&lt;/p&gt;&lt;/blockquote&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This is an old post, and no longer relevant as Juggernaut is
no longer a thing. Just use
&lt;a href="https://github.com/socketio/socket.io/"&gt;socket.io&lt;/a&gt; directly.&lt;/p&gt;
&lt;p&gt;I've fallen in love with
&lt;a href="https://github.com/maccman/juggernaut"&gt;Juggernaut&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Juggernaut gives you a realtime connection between your servers and
client browsers. You can literally push data to clients using your web
application, which lets you do awesome things like multiplayer gaming,
chat, group collaboration and more.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's built on node.js, &lt;a href="http://socket.io/"&gt;socket.io&lt;/a&gt;, and Redis and
makes implementing PubSub in your web apps super easy. (&lt;em&gt;How easy is
it?&lt;/em&gt;) This easy:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Client side&lt;/strong&gt; (subscriber)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;jug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Juggernaut&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;jug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;channel1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
    &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;New message: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Server side&lt;/strong&gt; (publisher - I'll use Python, but Ruby is similar)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;jug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Juggernaut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;jug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;channel&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;message&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Hello World!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I use Juggernaut to implement real time features in my
&lt;a href="http://flask.pocoo.org/"&gt;Flask&lt;/a&gt; application. Flask is a traditional web
framework and not very well suited to real-time. Juggernaut is a great
complement. My development environment is Windows, so I needed to get
Juggernaut running there. It wasn't terribly difficult but there were
enough gotchas that I thought I'd share how it's done (and that it can
be done).&lt;/p&gt;
&lt;p&gt;This assumes you have &lt;a href="http://www.python.org/download/releases/2.7.2/"&gt;Python 2.7
installed&lt;/a&gt;. I would not
recommend deploying Juggernaut in production on Windows. Here we go:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get Redis&lt;ul&gt;
&lt;li&gt;&lt;a href="http://redis.io/"&gt;Redis&lt;/a&gt; &lt;a href="http://redis.io/download"&gt;isn't
supported&lt;/a&gt; on Windows but there is a great
native port available that seems to work well enough. I wouldn't run
it in production, but it's been stable for me in development.
&lt;a href="https://github.com/dmajkic/redis/downloads"&gt;Download it here&lt;/a&gt; and
extract to a folder anywhere you please.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Get Node.js&lt;ul&gt;
&lt;li&gt;Node now supports Windows! Download and install it
&lt;a href="http://nodejs.org/#download"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Get Juggernaut&lt;ul&gt;
&lt;li&gt;You'll install &lt;a href="https://github.com/maccman/juggernaut"&gt;Juggernaut&lt;/a&gt;
via the Node Package Manager (npm). Npm is installed by default
with Node. Open a command window and navigate to the install
directory for Node (this is &lt;code&gt;C:\Program
Files (x86)\nodejs&lt;/code&gt; on my machine). Run &lt;code&gt;npm install -g juggernaut&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Get the Python library&lt;ul&gt;
&lt;li&gt;Armin Ronacher has written a &lt;a href="https://github.com/mitsuhiko/python-juggernaut"&gt;simple Python
library&lt;/a&gt; to connect
to Juggernaut. Install it by running &lt;code&gt;pip install
juggernaut&lt;/code&gt; at the Windows
command line. (&lt;a href="http://stackoverflow.com/questions/4750806/how-to-install-pip-on-windows"&gt;don't have
pip&lt;/a&gt;?)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Run it!&lt;ul&gt;
&lt;li&gt;Fire up &lt;code&gt;redis-server.exe&lt;/code&gt;
from the directory that you extracted Redis to.&lt;/li&gt;
&lt;li&gt;Run juggernaut from a Windows command line (just type &lt;code&gt;juggernaut&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Juggernaut helpfully comes with a nice little example
implementation (index.html). Npm installs it here by default: &lt;code&gt;C:\Users\AppData\Roaming\npm\node\_modules\juggernaut\public&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Not-so-helpfully, the test
implementation connects to Juggernaut over port 80, which is silly
because Juggernaut serves messages over port 8080. So crack open
that index.html file and change line 37 from "80" to "8080"&lt;/li&gt;
&lt;li&gt;Next, copy index.html and
application.js (which is the client side Juggernaut library) to a
directory in IIS so you can hit it locally. You do need to serve the
file from a web server - you can't just open the static file in a
browser because when the sample tries to connect to Juggernaut the
browser will view it as a cross-domain request and reject it.&lt;/li&gt;
&lt;li&gt;Now, browse to
http://127.0.0.1/index.html:
&lt;img alt="screen1" src="http://2.bp.blogspot.com/-gnoojdIsbBk/T05OuECnVSI/AAAAAAAAAFM/1Ut0DOb9n3M/s1600/jug_test.png"&gt;&lt;/li&gt;
&lt;li&gt;Almost done! Finally, fire up IDLE (or a python interpreter) and try
the following. You'll see the message immediately in the client app.
Cool!
&lt;img alt="jug_2" src="http://1.bp.blogspot.com/-n4TvFTCnnDk/T05Q4Pb3QJI/AAAAAAAAAFc/7nWDrQqaolk/s1600/jug_2.png"&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hope this helped some folks. Enjoy your shiny new real-time web component!&lt;/p&gt;</content><category term="Code &amp; Tutorials"></category></entry><entry><title>Communicating Performance Thresholds</title><link href="http://blog.untrod.com/2012/03/communicating-performance-thresholds.html" rel="alternate"></link><published>2012-03-01T22:52:00-08:00</published><updated>2012-03-01T22:52:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-03-01:/2012/03/communicating-performance-thresholds.html</id><summary type="html">&lt;p&gt;I think visually, so here's a way I like to communicate performance
thresholds. There are a bunch of dimensions that might make an
application behave differently. Test against those dimensions and plot
the thresholds of each one. Here are some that I made up, for a
theoretical application.&lt;/p&gt;


&lt;p&gt;&lt;img alt="slide1" src="http://3.bp.blogspot.com/-77vbiIFVUGY/T0_3mZHApWI/AAAAAAAAAFk/v9uPHmoHgc0/s1600/Slide1.GIF"&gt;&lt;/p&gt;
&lt;p&gt;The goal …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I think visually, so here's a way I like to communicate performance
thresholds. There are a bunch of dimensions that might make an
application behave differently. Test against those dimensions and plot
the thresholds of each one. Here are some that I made up, for a
theoretical application.&lt;/p&gt;


&lt;p&gt;&lt;img alt="slide1" src="http://3.bp.blogspot.com/-77vbiIFVUGY/T0_3mZHApWI/AAAAAAAAAFk/v9uPHmoHgc0/s1600/Slide1.GIF"&gt;&lt;/p&gt;
&lt;p&gt;The goal is to have all real world scenarios fit inside your polygon. So
here's a real world use of the application that we can handle no
problemo.&lt;/p&gt;
&lt;p&gt;&lt;img alt="slide2" src="http://4.bp.blogspot.com/-9cOm3wKXZE0/T0_3nvKgfII/AAAAAAAAAFs/fKECdQt3SV4/s1600/Slide2.GIF"&gt;&lt;/p&gt;
&lt;p&gt;And here's one that pushing the limits a bit more.&lt;/p&gt;
&lt;p&gt;&lt;img alt="slide3" src="http://4.bp.blogspot.com/-wmV-u7WcWOU/T0_3oEKV1qI/AAAAAAAAAF0/RqNJ5mOKHYI/s1600/Slide3.GIF"&gt;&lt;/p&gt;
&lt;p&gt;And here's one we didn't anticipate, or our app should never have
allowed because it's clearly not going to work.&lt;/p&gt;
&lt;p&gt;&lt;img alt="slide4" src="http://4.bp.blogspot.com/-fDR0VFPxQyM/T0_3oiGgjYI/AAAAAAAAAF8/kKwohNs8C_o/s1600/Slide4.GIF"&gt;&lt;/p&gt;
&lt;p&gt;There are a few caveats to visualizing performance thresholds this way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We might have missed a dimension. Turns out that the phase of the
    moon dramatically affects performance and that wasn't a dimension we
    tested along. Oops. This is no fault of the visualization, but can
    most definitely happen (and in fact did just happen and prompted
    this post).&lt;/li&gt;
&lt;li&gt;Dimensions might not be independent. For instance, if you have more
    memory, the maximum allowable job size may go way up. That's hard to
    capture here, and often isn't necessary. If you're giving guidance
    to a customer on performance, skip the nuance and stick to
    what works.&lt;/li&gt;
&lt;li&gt;We might not really understand some of the boundaries as well as
    this diagram indicates. With performance testing, it's rare that you
    can categorically say that 1,000 visitors works fine but 1,001
    visitors brings down your website. Build in some buffer
    when communicating.&lt;/li&gt;
&lt;li&gt;And remember, there is a difference between an app theoretically
    supporting a threshold and &lt;em&gt;actually testing&lt;/em&gt; to that threshold.
    Theoretical boundaries need be called out clearly and separately.&lt;/li&gt;
&lt;/ul&gt;</content><category term="Engineering Management"></category></entry><entry><title>Building Software and Building Bridges</title><link href="http://blog.untrod.com/2012/02/building-software-and-building-bridges.html" rel="alternate"></link><published>2012-02-21T14:57:00-08:00</published><updated>2012-02-21T14:57:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-02-21:/2012/02/building-software-and-building-bridges.html</id><summary type="html">&lt;p&gt;We have a problem. People can't get from one area of town to a
neighboring area because there is a river in between and no road. So
let's build a bridge.&lt;/p&gt;


&lt;h3&gt;Step 1: Requirements&lt;/h3&gt;
&lt;p&gt;We'll get detailed requirements from civil engineers and government
officials, including environmental constraints, safety guidelines,
traffic …&lt;/p&gt;</summary><content type="html">&lt;p&gt;We have a problem. People can't get from one area of town to a
neighboring area because there is a river in between and no road. So
let's build a bridge.&lt;/p&gt;


&lt;h3&gt;Step 1: Requirements&lt;/h3&gt;
&lt;p&gt;We'll get detailed requirements from civil engineers and government
officials, including environmental constraints, safety guidelines,
traffic considerations, and all sorts of other arcana.&lt;/p&gt;
&lt;h3&gt;Step 2: Design&lt;/h3&gt;
&lt;p&gt;We'll have architects, designers, and engineers create a plan that meets
all of the criteria. We'll do a tremendous amount of pre-planning. We'll
take public opinion surveys. We'll invest a huge amount of time and
energy up front to ensure that everyone knows exactly what kind of
bridge we're building and exactly how much it will cost. Our engineers
and designers will have estimates of exactly how many pounds of concrete
we'll need. And how many nails. And which suppliers the nails will be
coming from. And backup nail suppliers in case something happens to the
primary supplier.&lt;/p&gt;
&lt;h3&gt;Step 3: Implementation&lt;/h3&gt;
&lt;p&gt;Contractors go to work. Using the detailed design a team of
professionals can reasonably be expected to deliver this massive project
on time, and on budget. And we have our bridge!&lt;/p&gt;
&lt;p&gt;Hmmm...&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Requirements--&amp;gt; Design--&amp;gt; Implementation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Does that sound familiar? Oh yeah! It's &lt;a href="http://en.wikipedia.org/wiki/Waterfall_model"&gt;waterfall
development&lt;/a&gt;! And it
doesn't work for software.&lt;/p&gt;
&lt;p&gt;Since the beginning, we've tried to wrap traditional project management
and construction metaphors around software development. It sounds
reasonable - in both cases we have teams of designers and engineers
delivering a very complex, expensive solution over a long period of
time. In the construction world, if we're over budget or late, it's
probably due to some flaw in the requirements or design process. If only
we had invested more time up front! As software projects blow past their
budgets and deadlines
(&lt;a href="http://www.quotationspage.com/quote/723.html"&gt;whoosh&lt;/a&gt;!), companies
also often react by investing more up front. We get long, detailed
requirements documents that no one reads. Using construction techniques
to solve software problems doesn't work because it's a flawed analogy.&lt;/p&gt;
&lt;h3&gt;Let's try that bridge again, but with software&lt;/h3&gt;
&lt;p&gt;We're going to start by focusing on the problem to solve: get people
from A to B. With software, the solution isn't necessarily as obvious as
it is in the physical world. Maybe we need a bridge. But maybe we need a
ferry. Or a helicopter service. Or maybe we should just move the two
pieces of land closer together. Or freeze the river.&lt;/p&gt;
&lt;p&gt;Customers speak in terms of solutions: I want a bridge. I want a bigger
kitchen. But with software we know to be wary of this: unlike the
physical world, the users of software often do not have a good intuitive
understanding of what's possible. So while they speak in terms of
functionality and solutions, it's our job to root out the real problem
and come up with an appropriate solution.&lt;/p&gt;
&lt;p&gt;But let's say a bridge is the right answer and we've settled on the
bridge we want to build. It's big, it's complicated. Unlike building a
real bridge, spending lots and lots of time up front on our software
bridge will not lead to accurate estimates for our bridge's
construction. There are lots of reasons for this: programming is
fundamentally an individual task and developers work at dramatically
different levels of productivity. We may not know which developers are
working on our bridge. Or the ones that provide the estimate may not
deliver the parts they estimated.&lt;/p&gt;
&lt;p&gt;Furthermore, software complexity increases exponentially with the size
of the project. A 10,000 line program is hundreds of times more complex
than a 1,000 line program. But a 10,000sqft house is linearly more
complex than a 1,000sqft house*.&lt;/p&gt;
&lt;p&gt;The result is we have a unique statistical challenge when estimating
software. Michael Church on Hacker News &lt;a href="http://news.ycombinator.com/item?id=3522910"&gt;explains it
beautifully&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Let's say that you have 20 tasks. Each involves rolling a 10-sided
die. If it's a 1 through 8, wait that number of minutes. If it's a 9,
wait 15 minutes. If it's a 10, wait an hour.&lt;/p&gt;
&lt;p&gt;How long is this string of tasks going to take? Summing the median
time expectancy, we get a sum 110 minutes, because the median time for
a task is 5.5 minutes. The actual expected time to completion is 222
minutes, with 5+ hours not being unreasonable if one rolls a lot of
9's and 10's.&lt;/p&gt;
&lt;p&gt;This is an obvious example where summing the median expected time for
the tasks is ridiculous, but it's exactly what people do when they
compute time estimates, even though the reality on the field is that
the time-cost distribution has a lot more weight on the right. (That
is, it's more common for a "6-month" project to take 8 months than 4.
In statistics-wonk terms, the distribution is "log-normal".)&lt;/p&gt;
&lt;p&gt;Software estimates are generally computed (implicitly) by summing the
good-case (25th to 50th percentile) times-to-completion, assuming
perfect parallelism with no communication overhead, and with a
tendency for unexpected tasks, undocumented responsibilities, and bugs
to be overlooked outright.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Software teams will not be able to tell you the number of nails needed.
Or the pounds of concrete. Software estimates are unintuitive and must
account for a great degree of uncertainty.&lt;/p&gt;
&lt;p&gt;Good software teams use a completely different set of techniques to
battle uncertainty: They ship frequently (there are not many shippable
increments when building a bridge), run lots of experiments, and
integrate the design, implementation, validation, and requirements
phases together into one process. We encourage iterative, creative
thinking. We want to try lots of things, fail fast, and move on. It's
not unusual to read something like this about building software (from
&lt;a href="http://www.amazon.com/Little-Bets-Breakthrough-Emerge-Discoveries/dp/1439170428"&gt;Little
Bets&lt;/a&gt;):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Learn by doing. Fail quickly to learn fast. Develop experiments and
prototypes to gather insights, identify problems, and build up
creative ideas, like Beethoven did to in order to discover new musical
styles and forms.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's not surprising that the waterfall approach to software has failed.
Waterfall was taken from the world of construction, but construction is
simply not analogous to software. We need new processes.&lt;/p&gt;
&lt;p&gt;It's only in the last ten years with the emergence of Agile, Kanban,
rapid prototyping, and general acceptance of iterative, fail-fast
approaches that software development has come into its own. We are good
at building bridges, and we're finally getting good at building
software.&lt;/p&gt;
&lt;p&gt;* &lt;small&gt;I was a little uncomfortable
making this assertion without knowing much about construction, so I
called up a friend who is a project manager for medium/large sized
construction projects (industrial parks, housing developments, etc) and
discussed. He backed the claim.&lt;/small&gt;&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>No Bugs != Quality</title><link href="http://blog.untrod.com/2012/01/no-bugs-quality.html" rel="alternate"></link><published>2012-01-29T19:48:00-08:00</published><updated>2012-01-29T19:48:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-01-29:/2012/01/no-bugs-quality.html</id><summary type="html">&lt;p&gt;A low bug count is not a good indicator of quality software. Lack of
typos and grammatical errors is not indicative of a quality book.&lt;/p&gt;
&lt;p&gt;If your software has nagging "quality problems", driving the open bug
count to zero probably won't be much help (not least because it only
addresses …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A low bug count is not a good indicator of quality software. Lack of
typos and grammatical errors is not indicative of a quality book.&lt;/p&gt;
&lt;p&gt;If your software has nagging "quality problems", driving the open bug
count to zero probably won't be much help (not least because it only
addresses the bugs you've found - not the multitude inevitably lurking
in obscure code paths). If the software is hard to use; if it doesn't
solve the problem it was designed to solve; if it's slow; if you pay a
huge engineering tax whenever you make a change because the code base is
spaghetti; then bug count may not be your problem.&lt;/p&gt;
&lt;p&gt;In fact, a high bug count isn't even a reliable indicator of poor
quality. Google Chrome, the world's second most popular browser, has
over &lt;a href="http://code.google.com/p/chromium/issues/list?can=2&amp;amp;q=&amp;amp;colspec=ID+Pri+Mstone+ReleaseBlock+Area+Feature+Status+Owner+Summary&amp;amp;x=mstone&amp;amp;y=owner&amp;amp;cells=tiles"&gt;32 &lt;em&gt;thousand&lt;/em&gt; open
issues&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm all for keeping bug counts low - fewer bugs is better than more bugs
- but if you have a large number of infrequently-reported bugs, then bug
count is little more than a vanity metric. &lt;/p&gt;
&lt;p&gt;Fixing every outstanding edge-case bugs will not create happy users out
of &lt;a href="http://www.netpromoter.com/np/calculate.jsp"&gt;net detractors&lt;/a&gt;.
Playing whac-a-mole with bugs is ineffective. Quality issues demand
stepping back and taking &lt;a href="http://www.grahambrooks.com/blog/metrics-based-refactoring-for-cleaner-code/"&gt;different
approaches&lt;/a&gt;.
Bugs are symptoms, not causes.&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>Localization (or: Localisation) Tip</title><link href="http://blog.untrod.com/2012/01/localization-or-localisation-tip.html" rel="alternate"></link><published>2012-01-03T21:22:00-08:00</published><updated>2012-01-03T21:22:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-01-03:/2012/01/localization-or-localisation-tip.html</id><summary type="html">&lt;p&gt;I just learned something from our documentation team: Try to avoid using
gerunds in UI text because they are difficult to localize &amp;amp; translate.
While they certainly aren't a challenge for great translators, I'm told
they can be misinterpreted by mediocre translators - like the ones that
outsourced translating services might use …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I just learned something from our documentation team: Try to avoid using
gerunds in UI text because they are difficult to localize &amp;amp; translate.
While they certainly aren't a challenge for great translators, I'm told
they can be misinterpreted by mediocre translators - like the ones that
outsourced translating services might use.&lt;/p&gt;
&lt;p&gt;"Creating a page" --&amp;gt; "How to create a page"
"Click here for help resolving this issue" --&amp;gt; "Click here for help
with this issue"&lt;/p&gt;</content><category term="Engineering Management"></category></entry><entry><title>How to Improve Chrome Web App Adoption</title><link href="http://blog.untrod.com/2012/01/how-to-improve-chrome-web-app-adoption.html" rel="alternate"></link><published>2012-01-03T17:51:00-08:00</published><updated>2012-01-03T17:51:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-01-03:/2012/01/how-to-improve-chrome-web-app-adoption.html</id><summary type="html">&lt;p&gt;There was big news in the browser world this month, as Google's Chrome
browser &lt;a href="http://www.tomshardware.com/news/chrome-firefox-internet-explorer-browser,14147.html"&gt;eclipsed Firefox's market
share&lt;/a&gt;,
making is the #2 most popular browser in the world. Google's goals with
regard to Chrome have always been clear. As Peter Kasting, a founding
member of the Chrome team at Google …&lt;/p&gt;</summary><content type="html">&lt;p&gt;There was big news in the browser world this month, as Google's Chrome
browser &lt;a href="http://www.tomshardware.com/news/chrome-firefox-internet-explorer-browser,14147.html"&gt;eclipsed Firefox's market
share&lt;/a&gt;,
making is the #2 most popular browser in the world. Google's goals with
regard to Chrome have always been clear. As Peter Kasting, a founding
member of the Chrome team at Google, &lt;a href="https://plus.google.com/114128403856330399812/posts/9dKsD7Mi7JU"&gt;puts it&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;"The primary goal of Chrome is to make the web advance as much and as
quickly as possible"&lt;/p&gt;
&lt;p&gt;While I don't doubt this as the primary motivation for building the
browser, there are clearly some tangential opportunities that Google is
exploring. For instance, &lt;a href="http://ipullrank.com/googlebot-is-chrome/"&gt;there is a fascinating
theory&lt;/a&gt; that the Googlebot
(the crawler responsible for maintaining Google's search index) is in
fact running Chrome under the hood in order to index JavaScript and
other dynamic content.&lt;/p&gt;
&lt;p&gt;There is also the &lt;a href="http://en.wikipedia.org/wiki/Chrome_Web_Store"&gt;Chrome Web
Store&lt;/a&gt;, which has emerged
in over the last 12 months as a way for Chrome users to browser through
a (very) lightly curated selection of web "apps" and install them to
Chrome - an app store in the footsteps of Apple, for the browser &amp;amp; web.
The store appears to have about &lt;a href="http://www.chromeosapps.org/"&gt;65 million
users&lt;/a&gt; (though it's unclear what counts as
a user) and about 18,000 apps available.&lt;/p&gt;
&lt;p&gt;I have some theories about Google's strategic goals of the Chrome store
(in all likelihood it's to support &lt;a href="http://en.wikipedia.org/wiki/Google_Chrome_OS"&gt;Chrome
OS&lt;/a&gt;, but it might not be.
What else, say you? To drive Chrome adoption? To turn a profit by
pushing Google Checkout? To simply "advance the web"?) but ultimately we
can make the safe assumption that the success of the Chrome Web Store is
predicated on people installing and using Chrome apps. More app installs
= execution of strategy (whatever it is).&lt;/p&gt;
&lt;p&gt;So how is the store doing? With 65 million users, based on Chrome's
\~&lt;a href="http://gs.statcounter.com/"&gt;25% market share&lt;/a&gt; and the &lt;a href="http://www.internetworldstats.com/stats.htm"&gt;2bn worldwide
internet users&lt;/a&gt;, about 13%
of Chrome users have made use of the store. Given how the store works
today I'm actually surprised it's that high, but I'm guessing that
iPhone users' adoption of the Apple app store is close to 100% and that
the Google team is not satisfying mucking around at 13% adoption.&lt;/p&gt;
&lt;p&gt;Let's see if we can help them out.&lt;/p&gt;
&lt;p&gt;But first, it's worth noting that there are really easy ways to drive
Chrome app adoption: aggressively promote the store from within Chrome
itself, show app results in Google search results, and in general
promote the store across Google properties. However Google has wisely
resisted such cheap promotion in the past (edit: &lt;a href="http://searchengineland.com/googles-jaw-dropping-sponsored-post-campaign-for-chrome-106348"&gt;maybe
not&lt;/a&gt;),
so this discussion is focused on how the Chrome store can succeed on its
own merits, not on Google marketing it to a captive audience.&lt;/p&gt;
&lt;p&gt;So let's try and divine what exactly a "Chrome App" is, versus a
"normal" web app that's just run through the browser. I'll go ahead and
install the ever-present Angry Birds (which over 1m Chrome users have
also done). It's easy enough through the Chrome Store, and here's the
end result:&lt;/p&gt;
&lt;p&gt;&lt;img alt="chrome-app" src="http://3.bp.blogspot.com/-YbtPqp66Res/Tv3y4yPT9II/AAAAAAAAACY/fOZRT5FPas8/s1600/chrome+app.png"&gt;
I installed Angry Birds!&lt;/p&gt;
&lt;p&gt;Voilà! I now have an Angry Birds icon available whenever I open a new
Chrome tab.&lt;/p&gt;
&lt;p&gt;When I click it, Chrome navigates to http://chrome.angrybirds.com where
I can, surprise, surprise, play Angry Birds. It's worth noting that I
can just as easily hit this URL &lt;em&gt;without&lt;/em&gt; the Web App installed. In
fact, I can even hit in IE and it works fine.&lt;/p&gt;
&lt;p&gt;So what's the big deal? Surely there is something to Chrome Web Apps
other than a handy link on new browser tabs, right? Well, there is. The
difference is near the bottom of the screen, and here's what it says
when I have the app installed:&lt;/p&gt;
&lt;p&gt;&lt;img alt="installing-offline" src="http://3.bp.blogspot.com/-zEwt8dCBZNA/Tv31sIgTL5I/AAAAAAAAACk/X1wzKNGnUr8/s1600/installing+offline.png"&gt;&lt;/p&gt;
&lt;p&gt;Ah-ha! It's installing the offline version. And indeed, without the app
installed, I see this instead:&lt;/p&gt;
&lt;p&gt;&lt;img alt="offline-not-available" src="http://4.bp.blogspot.com/-i-yO5w7383I/Tv310-XsIkI/AAAAAAAAACw/vQD7NoKmAkE/s1600/offline+not+available.png"&gt;&lt;/p&gt;
&lt;p&gt;In fact, it's the same message I see in IE:&lt;/p&gt;
&lt;p&gt;&lt;img alt="angry-birds-ie" src="http://3.bp.blogspot.com/-qcMwM7A2Aes/Tv32GqlH2AI/AAAAAAAAAC8/wd-d5CKxxXI/s1600/angry+birds+IE.png"&gt;&lt;/p&gt;
&lt;p&gt;Hmmm, well that seems...minor. The handy shortcut is fine and good, and
apparently the app also gets access to some local storage, but if that's
it then the store may be in for a lot of comments like this, from
a Google's "&lt;a href="http://www.youtube.com/watch?v=yn_imZgWPtc"&gt;Why You Should Create an
App&lt;/a&gt;" video:&lt;/p&gt;
&lt;p&gt;&lt;img alt="glorified-bookmarks" src="http://2.bp.blogspot.com/-LpLe-9N7cGk/Tv4twS4w-wI/AAAAAAAAADI/G5uqDv8ccmI/s1600/glorified+bookmarks.png"&gt;&lt;/p&gt;
&lt;p&gt;But Angry Birds is just one example - there must be more, right? Let's
hear from Google directly, which will surely convince us that the app
store is more than a directory of useful web sites.&lt;/p&gt;
&lt;p&gt;Here is how Google describes the benefits of a web app, &lt;a href="http://code.google.com/chrome/webstore/faq.html#faq-gen-07"&gt;buried deep in
an FAQ&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why would someone install a web app instead of just using a bookmark
or typing in a URL?&lt;/strong&gt;
Installing an app ensures it is launchable from the New Tab Page via
the Chrome apps launcher. Installing an app also allows you to grant
it privileges such as unlimited local storage and background pages.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's pretty much the entirety of Google's explanation of why Chrome
apps exist. Even the way the question is posed makes the apps sound
ridiculous.&lt;/p&gt;
&lt;p&gt;To encourage app installs, the benefits of doing so need to be obvious
and tangible. Right now they are neither. There are in fact real
benefits to the app approach, but Google has seemingly little interest
in conveying them to end users. As a result, users view apps as
glorified bookmarks.&lt;/p&gt;
&lt;p&gt;The situation for developers is no better. The developer portal
&lt;a href="http://code.google.com/chrome/apps/docs/index.html"&gt;describes the
problem&lt;/a&gt; apps solve
as "requests for permission are annoying" (really, this is what it
says). Again, benefits are not spelled out and developers are left
guessing as to why, exactly, they would make an app.&lt;/p&gt;
&lt;p&gt;Ask an iOS developer what the benefits of a native app are (even shimmed
HTML5 apps made with technologies like PhoneGap). They'll say:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Frictionless purchasing&lt;/li&gt;
&lt;li&gt;In-app payments&lt;/li&gt;
&lt;li&gt;Rich APIs&lt;/li&gt;
&lt;li&gt;Exposure via the App Store&lt;/li&gt;
&lt;li&gt;Available offline/no network lag&lt;/li&gt;
&lt;li&gt;(and yes) Elevated permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In fact, Chrome Apps offer many of the same benefits but they are poorly
explained to both users and developers. Paid apps, integrated with
Google Wallet, &lt;a href="https://chrome.google.com/webstore/detail/cgllhajannolhgkllnfpapalgaioobkg"&gt;are
available&lt;/a&gt;
(albeit poorly adopted - this was literally the only example I could
find after 20 minutes of searching), as is an &lt;a href="http://www.youtube.com/watch?v=bAcyP06KqPs"&gt;in-app payments
API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Google just needs to explain this better. And it's not hard. Here's 10
minutes of effort in &lt;a href="http://www.balsamiq.com/"&gt;Balsamiq&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="chrome-app-benefits" src="http://1.bp.blogspot.com/-MmHMy-Buu88/TwI_2hxTCJI/AAAAAAAAAD4/FB5v-KK93pw/s1600/chrome+app+benefits.png"&gt;&lt;/p&gt;
&lt;p&gt;Google needs to make the benefits clearer.&lt;/p&gt;
&lt;p&gt;But even existing apps are not taking advantage of these benefits. For
instance, &lt;a href="http://www.prezi.com/"&gt;Prezi&lt;/a&gt; is a fairly popular
productivity app in the store, with about 50,000 users. As far as I can
tell the app is literally a glorified bookmark, just linking to
Prezi.com. I still have to sign up (create a username and password) for
Prezi.com, and if I want to purchase a paid version I get a form that
looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="sign-up" src="http://4.bp.blogspot.com/-3CgocWAcEvQ/TwM7kRYnp6I/AAAAAAAAAEE/PqNfndT4il0/s1600/sign+up.png"&gt;&lt;/p&gt;
&lt;p&gt;Wowza! Lots of form fields! Given that I have installed this as a Chrome
app, shouldn't Prezi know something about who I am, and have some sort
of access to my data? At best, signing up should be a one-click process
(similar to purchasing an app in the first place) and at worst, couldn't
it at least have an account already created for me and pre-populate some
of the fields? It's unclear to me how much of this is possible with the
current APIs, but this type of thing should be the default, expected
behavior for anything installed as an application (and only applications
that take advantage of the benefits of being an app should be featured
in the store). Rapid, secure registration and payment are clear benefits
to both users and developers of apps.&lt;/p&gt;
&lt;p&gt;If I &lt;em&gt;haven't&lt;/em&gt; installed Prezi as an app, Google should allow developers
to easily incorporate the "Add to Chrome" button to their site, which
would allow the site to then take advantage of easy registration and
payment options. Imagine if, above all of those Prezi registration
fields, there was a button:&lt;/p&gt;
&lt;p&gt;&lt;img alt="add-to-chrome-button" src="http://4.bp.blogspot.com/-E6Vtyf11ppQ/TwM8usR2uJI/AAAAAAAAAEQ/7WKgqskQjlI/s1600/add+to+chrome+button.png"&gt;&lt;/p&gt;
&lt;p&gt;A user could then choose between simply clicking this button, or filling
out then entire form. I'd click the button every time for rapid, safe,
and centralized payment and registration.&lt;/p&gt;
&lt;p&gt;Finally, the store itself needs better curation. Browsing around the
Games category, it seems half of the games are simply links to &lt;a href="http://www.chromegamebox.com/sports/sports201112098608.html"&gt;little
Flash
games&lt;/a&gt;
surrounded by ads - hardly the slick HTML5 "apps" Google wants to
promote. With a higher threshold for quality, and some improved
discovery features, the store could quickly become a great way to find
new services.&lt;/p&gt;
&lt;p&gt;In conclusion, the Chrome store has great potential but needs to make
changes to avoid becoming nothing more than a cool bookmarking service.
Specifically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Make the benefits of apps clearer to users and developers&lt;/li&gt;
&lt;li&gt;Create better registration and payment APIs, and help developers
    incorporate them into existing services&lt;/li&gt;
&lt;li&gt;Create an Add to Chrome button that can be used outside of the store
    itself&lt;/li&gt;
&lt;li&gt;Improve quality and discoverability of apps in the store&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I look forward to see how store adoption grows, and how this new take on
the app concept plays into Google's larger strategy and offerings.&lt;/p&gt;</content><category term="Everything Else"></category></entry><entry><title>DOET Shower</title><link href="http://blog.untrod.com/2012/01/doet-shower.html" rel="alternate"></link><published>2012-01-02T15:12:00-08:00</published><updated>2012-01-02T15:12:00-08:00</updated><author><name>Chris Clark</name></author><id>tag:blog.untrod.com,2012-01-02:/2012/01/doet-shower.html</id><summary type="html">&lt;p&gt;I stayed at a friend's apartment a few weeks ago. It is difficult to
describe the decor, but if my grandmother and Master P were forced to
collaboratively design an apartment, his place is what you might get.&lt;/p&gt;


&lt;p&gt;&lt;img alt="grandmother-bedroom" src="http://bowershouse.files.wordpress.com/2009/08/bedroom1.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="master-p" src="http://www.vladtv.com/images/size_fs/video-189495.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Walls covered in paint made from melted silver, gold dragon faucet heads …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I stayed at a friend's apartment a few weeks ago. It is difficult to
describe the decor, but if my grandmother and Master P were forced to
collaboratively design an apartment, his place is what you might get.&lt;/p&gt;


&lt;p&gt;&lt;img alt="grandmother-bedroom" src="http://bowershouse.files.wordpress.com/2009/08/bedroom1.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="master-p" src="http://www.vladtv.com/images/size_fs/video-189495.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Walls covered in paint made from melted silver, gold dragon faucet heads
roaring out of the wet-bar sink, all trimmed with flower-print or
&lt;a href="https://www.google.com/search?q=toile&amp;amp;tbm=isch"&gt;toile&lt;/a&gt; curtains. That
sort of thing.&lt;/p&gt;
&lt;p&gt;The shower is, of course, also over-the-top; one of those awesome luxury
showers with heads and nozzles jutting out every which-way from the
granite walls. Sounds like great fun, but when it came time to actually
use it, I was confronted with the following:&lt;/p&gt;
&lt;p&gt;&lt;img alt="shower" src="http://4.bp.blogspot.com/-FbYKI5E44KM/TwHFD0HvxWI/AAAAAAAAADU/oca5LZ5ombE/s1600/shower.JPG"&gt;&lt;/p&gt;
&lt;p&gt;Not only did I fail to figure out how to control the temperature and
which shower heads were active, but I couldn't even figure out how to
turn the thing on and off! I was assaulted from all sides by water of
all different temperatures during the minutes of fiddling it took for me
to turn this stupid thing off.&lt;/p&gt;
&lt;p&gt;I was horrified enough by this set of controls that I actually bothered
to take this photo to document the insanity.&lt;/p&gt;
&lt;p&gt;Yesterday I started reading The Design of Everyday Things by Don Norman
and, to my delight, came upon the following diagram:&lt;/p&gt;
&lt;p&gt;&lt;img alt="diagram" src="http://1.bp.blogspot.com/-DJ3B3Zn6ut4/TwHH06fuTxI/AAAAAAAAADg/oYsaxl8m5_E/s1600/shower+3.png"&gt;&lt;/p&gt;
&lt;p&gt;With the wonderful caption: "What do you do here? Why would anyone dream
up this scheme?"&lt;/p&gt;
&lt;p&gt;I am very much looking forward to reading this book.&lt;/p&gt;</content><category term="Everything Else"></category></entry></feed>