<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>cmos</title>
    <description>Christian Moscardi&apos;s personal website and blog.
</description>
    <link>https://www.christianmoscardi.com/</link>
    <atom:link href="https://www.christianmoscardi.com/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Mon, 25 Aug 2025 19:07:35 +0000</pubDate>
    <lastBuildDate>Mon, 25 Aug 2025 19:07:35 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Finding the mode with claude code</title>
        <description>&lt;style&gt;
code {
  white-space: pre-wrap;
}
&lt;/style&gt;

&lt;p&gt;I’m preparing to release an open-source package – the details don’t matter very much, but it’s a UX to fine-tune OCR models. As I’m prepping for release, I decide I should have a landing page. I am definitely on the AI coding assistant train (handing over my $20/month to anthropic, for claude code), and as such I make a vague request for it to give me something that &lt;em&gt;isn’t&lt;/em&gt; just the default scaffolded next.js landing page.&lt;/p&gt;

&lt;p&gt;Here is what I ask:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; update src/app/page.tsx to be more descriptive and not use the default next.js styles
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I do this with some curiosity, as it’s obviously a generic prompt. What does “be more descriptive” mean? What styles would it pick by default? Well, here’s what I get back:&lt;/p&gt;

&lt;div style=&quot;width: 100%;&quot;&gt;
  &lt;img style=&quot;width: 100%;&quot; src=&quot;/static/images/saas_landing_page.png&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Dear god, it’s a SaaS marketing page. Trust me, if you scroll down, it’s nearly pitch-perfect.&lt;/p&gt;

&lt;p&gt;When I put in this prompt, I had limited-to-no expectations about what would come out. I just figured prompting would be an easy way to get something different, a starting point that I could work from. The only point I wanted to make with this post, really, is that I guess the internet’s modal landing page (conditioned on being a next.js app for AI/ML etc) is a SaaS marketing page. Or, more likely, the modal user of Claude Code is making a SaaS, Anthropic knows this, and has tuned their model, updated their training data, etc. appropriately. It was pretty interesting to see regardless.&lt;/p&gt;

&lt;p&gt;Of course, I then updated my prompt to get what I actually wanted. However, &lt;em&gt;I hadn’t known what I had wanted&lt;/em&gt; prior to seeing this SaaS page. This gave me a bit of a creative jump start, similar to how you might &lt;a href=&quot;https://dribbble.com/tags/landing-page&quot;&gt;go scroll dribbble&lt;/a&gt; for some ideas. And that was helpful.&lt;/p&gt;

</description>
        <pubDate>Thu, 21 Aug 2025 13:20:00 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2025/08/21/ai-modal-landing-page.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2025/08/21/ai-modal-landing-page.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Fine-tuning OCR works really well: the Statistical Abstracts of the United States</title>
        <description>&lt;style&gt;
td {
    text-align: right;
}
&lt;/style&gt;

&lt;p&gt;Several years ago, a friend working on his PhD asked if I could help digitize the &lt;a href=&quot;https://www.census.gov/library/publications/time-series/statistical_abstracts.html&quot;&gt;Statistical Abstracts of the United States&lt;/a&gt; in order to spare him the hassle of going through these documents page-by-page. These abstracts are a historical Census Bureau dataset that includes many interesting things, like how many chickens were in the United States in 1920. If you want to know, there were ~360 million chickens in the United States in 1920. (The Chicken Census is now conducted and published by USDA.)&lt;/p&gt;

&lt;p&gt;Here’s what they look like:&lt;/p&gt;

&lt;figure style=&quot;display: block; margin-left: auto; margin-right: auto; width: 90%;&quot;&gt;
&lt;img alt=&quot;image of statistical abstract table with cheeky caption&quot; src=&quot;/static/images/stat_abstracts/pg1_abstract_overview.png&quot; style=&quot;width: 100%;&quot; /&gt;
&lt;/figure&gt;

&lt;p&gt;The Census Bureau has scanned these documents – so in that sense, they are already digitized – but has not attempted to perform OCR (to make the documents easily searchable) or publish the tables in a digitally native format (to make the data easier to work with). I wanted to give it a try. I’m not sure why, but I find this problem captivating – it really &lt;em&gt;feels&lt;/em&gt; like something computers should be able to do well.&lt;/p&gt;

&lt;h2 id=&quot;what-did-i-want-to-do&quot;&gt;What did I want to do?&lt;/h2&gt;
&lt;p&gt;To start, I wanted to make these into “searchable PDFs” – essentially, OCR the content, and especially the numbers, at a high quality. Presumably this would, at the very least, help my friend find the numbers he’s looking for more easily.&lt;/p&gt;

&lt;p&gt;In addition to simply OCRing these abstracts, I would love to be able to extract and convert the tables into a machine-readable tabular format (ie csv) to make them easier to work with.&lt;/p&gt;

&lt;h2 id=&quot;what-toolsservices-did-i-look-at&quot;&gt;What tools/services did I look at?&lt;/h2&gt;
&lt;p&gt;When my friend asked me if I could do this (circa 2019), I gave &lt;a href=&quot;https://github.com/tesseract-ocr/tesseract&quot;&gt;Tesseract&lt;/a&gt; a go, and found the results left something to be desired. I fairly quickly put it on the shelf, as it became clear that high-quality results would take a lot of effort.&lt;/p&gt;

&lt;p&gt;More recently (circa 2024), I’ve been peripherally involved with the Decennial Census Digitization and Linkage (&lt;a href=&quot;https://www.census.gov/programs-surveys/dcdl.html&quot;&gt;DCDL&lt;/a&gt;) project, which is in the process of digitizing the Census records from 1960, 1970, 1980, and 1990. This gave me an excuse to investigate the latest and greatest in OCR technology – and things have improved considerably.&lt;/p&gt;

&lt;p&gt;In exploring the world of OCR, I discovered a variety of libraries and services doing OCR and related activities. In addition, Microsoft, Google, and Amazon have offerings in this universe through their cloud platforms. All groups seem to be using the latest and greatest in deep learning, plus their own special sauce.&lt;/p&gt;

&lt;p&gt;A bit of digging and preliminary exploration led me to narrow down my interests to two major contenders: Microsoft and DocTR. In particular, DocTR has a &lt;a href=&quot;https://mindee.github.io/doctr/latest//using_doctr/using_models.html#end-to-end-ocr&quot;&gt;helpful comparison table&lt;/a&gt; in their own documentation – thanks DocTR/Mindee (parent company) for making a great package!&lt;/p&gt;

&lt;p&gt;I didn’t consider any layout recognition (ie identifying elements like titles, paragraphs, etc), table extraction (ie turning a table image into machine-readable, tabular data), or other related packages/models yet – I wanted to get a strong baseline of OCR to start. Given the goal I outlined, I’m definitely interested in table extraction from these documents in the future.&lt;/p&gt;

&lt;h2 id=&quot;starting-out-doctr&quot;&gt;Starting out: DocTR&lt;/h2&gt;
&lt;p&gt;To start, I figured I’d explore DocTR a bit. It is open-source, easy-to-use, performant, and state-of-the-art (&lt;a href=&quot;https://github.com/mindee/doctr/tree/main?tab=readme-ov-file#models-architectures&quot;&gt;their README&lt;/a&gt; references all the papers they’ve implemented).&lt;/p&gt;

&lt;p&gt;In its most basic case (a nicely aligned, typed document), DocTR does its OCR via a two-step process:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Draw a box around every word in the image (OCR jargon: &lt;strong&gt;text detection&lt;/strong&gt;)&lt;/li&gt;
  &lt;li&gt;Recognize the text in each box (OCR jargon: &lt;strong&gt;text recognition&lt;/strong&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each of these tasks is performed by its own model – DocTR has a variety of models you can plug in for either task.&lt;/p&gt;

&lt;h2 id=&quot;attempt-1-getting-started&quot;&gt;Attempt #1: Getting Started&lt;/h2&gt;
&lt;p&gt;As one does with any new package they’re getting acquainted with, my first attempt was to follow the “Getting Started” section of DocTR’s README. Something like this:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;doctr.io&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DocumentFile&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;doctr.models&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ocr_predictor&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;model&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ocr_predictor&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pretrained&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DocumentFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_pdf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;1940_statistical_asbtract.pdf&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Lots of credit to DocTR – they have sensible defaults and make it easy to get started.&lt;/p&gt;

&lt;p&gt;The results were pretty good. In the below image, however, you can immediately see an issue: the text detection is grabbing blocks that are too big. Each box should cover a single word (or in these tables, a single number). I drew a couple of red arrows pointing to an example box that is too big.&lt;/p&gt;

&lt;p&gt;&lt;img style=&quot;width: 90%;&quot; src=&quot;/static/images/stat_abstracts/doctr_naive_result.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Looking at the actual text, there appears to be plenty of room for improvement as well. The results below, from the caption above the table, speak for themselves.&lt;/p&gt;

&lt;table style=&quot;background-color: white;&quot;&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img style=&quot;width: 100%;&quot; src=&quot;/static/images/stat_abstracts/caption_note_ocr_example.png&quot; /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; color: black;&quot;&gt;

NOTE.-Gross aroas include surfaco ofi inland wators axcept Groat Lakes, Strait of Juan do Fuca, and Strait
of Georgia. Rovisions due to greator accuracy of curront maps. For dates of organization of States
and Territories, soo tablo irgt p. 1, Statistical Abstract, 1931, and previous issues
&lt;/td&gt;

&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;While my US geography could certainly be better, the last time I looked I didn’t see any Groat Lakes on the map.&lt;/p&gt;

&lt;h2 id=&quot;attempt-2-cropping&quot;&gt;Attempt #2: cropping&lt;/h2&gt;
&lt;p&gt;After fiddling with various parameters, trying different models from DocTR’s model zoo, etc. , I noticed something about the results. DocTR &lt;em&gt;seemed&lt;/em&gt; to do better on the larger text in the scans. Furthermore, an initial preprocessing step for the detection model is to standardize image size (I believe to 1024x1024). These scans have margins of 2-300px of whitespace, which means that whitespace is taking up valuable real-estate in the standardized image. I developed an extremely low-budget cropping algorithm, based on the fact that these scans are b/w and relatively clean. I’ll spare you the details, but here is the before/after.&lt;/p&gt;

&lt;p&gt;&lt;img style=&quot;width: 100%&quot; src=&quot;/static/images/stat_abstracts/cropping_algo_results.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Could definitely do better, but we’ve cleared out most of the junky whitespace. Hopefully this gives the model more signal to work from.&lt;/p&gt;

&lt;p&gt;Looking at the same result as earlier, it seems cropping helps! Notice the oversized boxes are gone. Interestingly, the text recognition looked a bit better too… probably the result of better bounding boxes.&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
&lt;td style=&quot;width:50%;&quot;&gt;
&lt;img style=&quot;width: 90%;&quot; src=&quot;/static/images/stat_abstracts/doctr_naive_result_cropped.png&quot; /&gt;
&lt;p&gt; Before cropping: oversized detection boxes don&apos;t work.&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;img style=&quot;width: 90%&quot; src=&quot;/static/images/stat_abstracts/crop_doctr_result_cropped.png&quot; /&gt;
&lt;p&gt; After cropping: oversized detection boxes are gone.&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;h2 id=&quot;attempt-3-fine-tuning&quot;&gt;Attempt #3: fine-tuning&lt;/h2&gt;
&lt;p&gt;While some preprocessing tricks were &lt;em&gt;helping&lt;/em&gt;, I still had issues. The detection wasn’t fantastic on more difficult tables/pages, and numbers were not super high quality. In particular, the OCR recognition’s (ie word -&amp;gt; text) initial accuracy was 89% (spoiler alert: I hand labeled a ground-truth dataset to come up with this number, and do the fine-tuning). While the text example I gave above might be correctable via clever spell-checking, post-processing, or may not even matter… incorrect numbers would render digital versions of these documents substantially less useful.&lt;/p&gt;

&lt;table style=&quot;width: 100%;&quot;&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;
&lt;img style=&quot;width:60%;&quot; src=&quot;/static/images/stat_abstracts/difficult_table_before.png&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;
In this example, the detection is combining numbers across table columns in weird ways... and in other places, splitting up numbers inappropriately. This frequently happens in the images where these tables have little margin.
&lt;/p&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;img style=&quot;width: 100%;&quot; src=&quot;/static/images/stat_abstracts/bad_number_examples.png&quot; /&gt;
&lt;p style=&quot;text-align: left;&quot;&gt;
Some example misrecognized numbers. It really is hard to tell with some of these scans.
&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;SO, it was time to get my hands dirty and attempt to fine-tune the models DocTR uses. This also gave me the opportunity to do this as a real data project – set up quality metrics for the different parts of the pipeline I care about, measure those metrics, and hopefully improve upon them.&lt;/p&gt;

&lt;p&gt;As mentioned above, DocTR uses a 2-stage pipeline, which means we have 2 models to fine-tune: the model for drawing the bounding boxes (text detection), and the model for actually OCRing the text inside each of those boxes (text recognition).&lt;/p&gt;

&lt;p&gt;DocTR has reasonably nice infrastructure for fine-tuning their models. I used &lt;a href=&quot;https://labelstud.io&quot;&gt;Label Studio&lt;/a&gt; to clean up the bounding boxes and to create the recognition ground-truth dataset.&lt;/p&gt;

&lt;h3 id=&quot;fine-tuning-text-detection-the-bounding-boxes&quot;&gt;Fine tuning text detection (the bounding boxes)&lt;/h3&gt;

&lt;p&gt;I’ll spare you the gory details and just give you the before/after. I created a dataset of just 9 pages’ worth of bounding boxes. Let me tell you, this was very unpleasant to do.&lt;/p&gt;

&lt;p&gt;Here are performance metrics on a held-out validation page:&lt;/p&gt;
&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;Recall&lt;/th&gt;
  &lt;th&gt;Precision&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;Before fine-tuning&lt;/td&gt;
  &lt;td&gt;86%&lt;/td&gt;
  &lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;After fine-tuning&lt;/td&gt;
  &lt;td&gt;97%&lt;/td&gt;
  &lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;The numbers are up! Better precision means that when we predict a bounding box, it is correct, and better recall means we find more of the bounding boxes in our ground-truth dataset. Here’s an example (out-of-sample) table before/after fine-tuning (and illustrating some of the wonky number splitting in the baseline model):&lt;/p&gt;

&lt;center&gt;
&lt;table style=&quot;width: 50%; height: 2em;&quot;&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;
&lt;img src=&quot;/static/images/stat_abstracts/out_of_sample_table_before.png&quot; /&gt;
&lt;p&gt;Before fine-tuning&lt;/p&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;
&lt;img src=&quot;/static/images/stat_abstracts/out_of_sample_table_after.png&quot; /&gt;
&lt;p&gt;After fine-tuning -- much better&lt;/p&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/center&gt;
&lt;p&gt;On the left (before fine-tuning), you can the see too-large boxes and split numbers that I’ve been highlighting in other examples – this won’t do! After fine-tuning, on the right, things look pretty much perfect.&lt;/p&gt;

&lt;p&gt;Remember, this was just 9 pages’ worth of labelled data. (Though admittedly each page has a lot of bounding boxes to work with). I also think I could better tune hyperparemeters to squeeze a little more performance out of the model. Finally, the model does give a score of how confident it is (an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;objectness_score&lt;/code&gt;), though I primarily see post-processing QA applications for this score vs. using it to improve model performance.&lt;/p&gt;

&lt;h3 id=&quot;fine-tuning-text-recognition-the-actual-ocr&quot;&gt;Fine tuning text recognition (the actual OCR)&lt;/h3&gt;
&lt;p&gt;After getting detection working better, it was time to fine-tune OCR. The labelling process for this one was much easier, and this is generally a simpler ML problem (given an image representing a word, output the word). I took a sampling of about 300 words to label, specifically targeting records the model wasn’t confident on (we get a confidence score for each prediction).&lt;/p&gt;

&lt;p&gt;Here are some example images. Again, these are crops from the pages, as determined by the bounding boxes we get back from detection. Note how the first image has some sort of artifact in the scan that makes what &lt;em&gt;should&lt;/em&gt; be a decimal look like… a slash, or something else. The model was not very confident on this image.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/stat_abstracts/example_ocr_images.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Again, sparing you the gory details of hand-labelling and training, here are the results on a held-out validation set:&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;Accuracy&lt;/th&gt;
  &lt;th&gt;# correct&lt;/th&gt;
  &lt;th&gt;# samples&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;Before fine-tuning&lt;/td&gt;
  &lt;td&gt;54%&lt;/td&gt;
  &lt;td&gt;20&lt;/td&gt;
  &lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;After fine-tuning&lt;/td&gt;
  &lt;td&gt;97%&lt;/td&gt;
  &lt;td&gt;36&lt;/td&gt;
  &lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;In particular, we got 36/37 on the validation set correct! And the one missed was the first example image with the messy scan artifact. In a production application, it’d be easy enough to use model confidence (the model remained low-confidence on that image) to flag concerning images for human review. Also worth noting that the single error was incorrectly identifying the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; character as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:&lt;/code&gt; (something we could probably fix with post-processing).&lt;/p&gt;

&lt;h2 id=&quot;end-to-end&quot;&gt;End-to-end&lt;/h2&gt;
&lt;p&gt;Now, having fine-tuned both stages of the pipeline, I looked at the results in totality to see how well we could actually OCR pages (you know, the thing we actually care about). Here is how things look overall with detection in our first table:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/stat_abstracts/finetuned_result.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;And, our example text. Much better.&lt;/p&gt;
&lt;table style=&quot;background-color: white;&quot;&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;img style=&quot;width: 100%;&quot; src=&quot;/static/images/stat_abstracts/caption_note_ocr_example.png&quot; /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left; color: black;&quot;&gt;
NOTE. - Gross areas include surface of inland waters Gxcept Great Lakes, Strait of Juan de Fuca, and Strait
of Georgia. Revisions arc due to greater accuracy of current maps. For dates of organization of States and Territories, see table 2, p. 1, Statistical Abstract, 1931, and previous issues.
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;h3 id=&quot;the-bottom-line-how-good-is-it&quot;&gt;The bottom line: how good is it?&lt;/h3&gt;

&lt;p&gt;Finally, I hand-labelled a full page’s worth of recognition images to be able to evaluate the end-to-end performance of the finetuned pipeline on a full page. This was much easier than the first round of hand-labelling, because the fine-tuned model is so good!&lt;/p&gt;

&lt;p&gt;Here are the results. A massive improvement.&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;Recall&lt;/th&gt;
  &lt;th&gt;Precision&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;Before fine-tuning&lt;/td&gt;
  &lt;td&gt;61%&lt;/td&gt;
  &lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;After fine-tuning&lt;/td&gt;
  &lt;td&gt;92%&lt;/td&gt;
  &lt;td&gt;91%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;If you’ve been following, you would have noticed the finetuned detection has precision/recall of roughly 97% on each page, and the recognition is 99% accurate. However, combining the two steps essentially compounds errors in both processes, leading to slightly lower, but still very good, numbers.&lt;/p&gt;

&lt;p&gt;As an aside, while I was hand-labelling a page for end-to-end results, I couldn’t believe some of the characters the finetuned OCR was able to recognize. I had to go look at a whole bunch of context to confirm these superscript characters referencing footnotes, for example.&lt;/p&gt;
&lt;center&gt;
&lt;table&gt;
&lt;tr&gt;

&lt;td style=&quot;text-align: center; width: 50%;&quot;&gt;
&lt;img src=&quot;/static/images/stat_abstracts/how_did_it_get_this.png&quot; style=&quot;width: 50%;&quot; /&gt;
&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;
&lt;img src=&quot;/static/images/stat_abstracts/how_did_it_get_this_2.png&quot; /&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/center&gt;

&lt;h2 id=&quot;commercial-ocr-microsoft-comparison&quot;&gt;Commercial OCR (Microsoft) comparison&lt;/h2&gt;
&lt;p&gt;So, DocTR (with fine-tuning) looks really good, and is relatively easy. However, according to &lt;a href=&quot;https://mindee.github.io/doctr/using_doctr/using_models.html#end-to-end-ocr&quot;&gt;DocTR’s own documentation&lt;/a&gt;, Microsoft has the best OCR at the moment (at least among the big 3 cloud service providers). If my PhD-seeking friend really wants to digitize the Statistical Abstracts of the United States, should he just pay Microsoft? Let’s find out.&lt;/p&gt;

&lt;p&gt;First off, Microsoft’s object detection looks really good at first blush. Here is the first table, with bounding boxes drawn on it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/stat_abstracts/ms_ocr_result.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;However, one issue will cause challenges for digitizing these documents: it separates numbers out into constitutent components. We could potentially fix this with post-processing, but it is more effort.&lt;/p&gt;

&lt;p&gt;The OCR is pretty good. It was &lt;em&gt;perfect&lt;/em&gt; on the “Gross areas include…” text example I’ve been using through this post – I think Microsoft is running through a spellchecker (or some LLM thing or whatever). That said, similar to DocTR, it does mistake some numbers in the table. For example, it mistakes the 3 for a 9 in the very first statistic. Here is the table from Microsoft (in addition to straight up OCR, Microsoft does table extraction, and it is pretty good, so enjoy!). You can compare its results to the screenshot above and find a few errors. I bolded a couple that I noticed. You can also see the spaces between numbers because they were detected as separate tokens.&lt;/p&gt;

&lt;table class=&quot;table-view&quot; style=&quot;width: 100%; font-size: 14px;&quot;&gt;
   &lt;tbody&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         ACCESSION
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Date
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Gross &lt;b&gt;aroa&lt;/b&gt;, square miles
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         ACCESSION
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Date
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Gross area, square milos
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Aggregate (1930)
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         &lt;b&gt;3, 798, 395&lt;/b&gt;
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Outlying territories and posses-
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         sions
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         711,606
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Continental United States
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         3,026,789
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Alaska Territory
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1867
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         586, 400
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Territory in 1790 1
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         892, 135
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Hawaii Territory
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1898
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         6,407
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Louisiana Purchase
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1803
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         827,987
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Philippine Islands
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1899
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         114, 400
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Florida
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1819
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         58, 666
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Puerto Rico
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1899
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         3,435
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         By treaty &lt;b&gt;witd&lt;/b&gt; Spain.
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1819
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         13, 435
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         &lt;b&gt;Quam&lt;/b&gt;
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1899
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         206
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Texas
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1845
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         389, 166
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         American Samoa.
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1900
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         &lt;b&gt;7G&lt;/b&gt;
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Oregon
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1846
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         286, 541
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Panama Canal Zone.
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1904
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         &lt;b&gt;649&lt;/b&gt;
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Mexican Cession
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1848
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         529, 189
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Virgin Islands of tde U. S ..
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1917
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         133
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         Gadsden Purchase
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         1853
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
        &lt;span&gt;
         29, 670
        &lt;/span&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
     &lt;td&gt;
      &lt;div class=&quot;ms-Stack css-106&quot;&gt;
       &lt;div class=&quot;ms-Stack-inner css-324&quot;&gt;
       &lt;/div&gt;
      &lt;/div&gt;
     &lt;/td&gt;
    &lt;/tr&gt;
   &lt;/tbody&gt;
  &lt;/table&gt;

&lt;p&gt;I compared its detection performance to my ground-truth dataset. MS is definitely better out-of-the-box. AFAIK you cannot fine-tune Microsoft’s OCR models (you can create custom models for information extraction tasks, but that is another blog post).&lt;/p&gt;

&lt;p&gt;These results somewhat underrepresent MS’ performance, in my opinion – in particular, MS’ detection is separating out single numbers into multiple tokens as shown above. That said, finetuning gets it 96% right without any further effort required (vs. needing to do post-processing with MS’ detection to combine those separated numbers).&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;Detection recall&lt;/th&gt;
  &lt;th&gt;Detection precision&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;MS (out of the box, all pages)&lt;/td&gt;
  &lt;td&gt;82%&lt;/td&gt;
  &lt;td&gt;62%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (out of the box, all pages)&lt;/td&gt;
  &lt;td&gt;59%&lt;/td&gt;
  &lt;td&gt;36%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;MS (out of the box, validation page only)&lt;/td&gt;
  &lt;td&gt;71%&lt;/td&gt;
  &lt;td&gt;49%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (finetuned, validation page only)&lt;/td&gt;
  &lt;td&gt;97%&lt;/td&gt;
  &lt;td&gt;96%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;I attempted to evaluate MS’ recognition on its own merits by comparing instances where my labeled bounding boxes overlapped MS’ bounding boxes closely enough that they would be recognizing basically the same text. The main places the bounding boxes don’t overlap “enough” to be comparable is the table numbers (where MS breaks them apart)… which is also where both MS and out-of-the-box DocTR perform the worst. So overall MS and out-of-the-box DocTR are probably a little worse than represented by these numbers.&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;Model&lt;/th&gt;
  &lt;th&gt;Recognition Accuracy &lt;br /&gt; (on words in ~200 overlapping, hand-labelled bounding boxes)&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;MS (out-of-the-box)&lt;/td&gt;
  &lt;td&gt;87%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (out-of-the-box)&lt;/td&gt;
  &lt;td&gt;71%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (fine-tuned)&lt;/td&gt;
  &lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;And finally, end-to-end performance on the validation page. Again, MS is underrated here because of its issue with separating out numbers in detection. But if I were making a professional recommendation on how to do this… I’d fine-tune.&lt;/p&gt;

&lt;table&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;Recall&lt;/th&gt;
  &lt;th&gt;Precision&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (out-of-the-box)&lt;/td&gt;
  &lt;td&gt;61%&lt;/td&gt;
  &lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;DocTR (fine-tuned)&lt;/td&gt;
  &lt;td&gt;92%&lt;/td&gt;
  &lt;td&gt;91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;MS (out-of-the-box)&lt;/td&gt;
  &lt;td&gt;56%&lt;/td&gt;
  &lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;

&lt;h2 id=&quot;conclusion--next-steps&quot;&gt;Conclusion + next steps&lt;/h2&gt;
&lt;p&gt;Fine-tuning works &lt;em&gt;really&lt;/em&gt; well. We were able to boost OCR performance on noisy historical documents by a good margin, with minimal effort. A fine-tuned pipeline significantly outperforms a well-regarded commercial offering.&lt;/p&gt;

&lt;p&gt;While, sadly, my friend has completed his PhD, I think I would be comfortable giving him the OCR’d statistical abstracts using this pipeline, and would imagine it’d have made his life a little easier. Sorry I’m late, Alex.&lt;/p&gt;

&lt;p&gt;Going forward, I am thinking I’ll:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Use this pipeline (maybe with a little more fine-tuning if necessary) to digitize the corpus of abstracts.&lt;/li&gt;
  &lt;li&gt;Investigate table extraction and see how well I can make that work well enough.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Furthermore, we can measure performance and identify low-confidence predictions for further fine-tuning, human intervention, or other correction if necessary. With MS’ commercial offering, you can identify low-confidence predictions, but have to fix them with post-processing and can’t fine-tune their OCR models. I can imagine a human-in-the-loop pipeline that makes this entire process easy, improving the model with human feedback and giving metrics about digitization quality based on minimal human labelling.&lt;/p&gt;

&lt;p&gt;With that in mind, I want to work on a toolkit to make this really easy to do for any set of documents. If you’re interested in using such a thing, I’m looking for use cases, so feel free to reach out to &lt;a href=&quot;mailto:christianmoscardi@gmail.com&quot;&gt;christianmoscardi@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you’ve gotten this far, source is here: &lt;a href=&quot;https://github.com/cmoscardi/stat-abstract-explore&quot;&gt;https://github.com/cmoscardi/stat-abstract-explore&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclaimer: While I work at the Census Bureau, this was a side project for me. I’ve received no money or support to work on this, all views in the post are my own, etc. etc.&lt;/p&gt;
</description>
        <pubDate>Thu, 03 Oct 2024 09:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2024/10/03/digitizing-us-statistical-abstracts.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2024/10/03/digitizing-us-statistical-abstracts.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>How well do DC&apos;s red-painted bus lanes work?</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://ddot.dc.gov/page/h-i-streets-nw-bus-lane-pilot&quot;&gt;Starting June 3&lt;/a&gt; and ending September 27, DDOT piloted two bus lanes on H and I streets. In particular, the District painted the rightmost lanes of these streets red, and dedicated those lanes to buses between 7AM to 9:30AM and 4PM to 6:30PM. But, did those shiny, fresh coats of paint actually do anything? According to DDOT, “Preliminary data comparing June 2018 and June 2019 found improvements in bus speeds and reliability on I Street NW &lt;em&gt;but not on H Street NW&lt;/em&gt;.” Let’s dig into that a little bit, especially keeping in mind that in NYC, the 14th street bus lanes saw &lt;a href=&quot;https://ny.curbed.com/2019/10/18/20919729/new-york-transportation-bus-lane-transit-priority&quot;&gt;immediate, dramatic improvements&lt;/a&gt; upon closing an entire road to traffic. Do visual demarcations that close a single lane to traffic have the same effect?&lt;/p&gt;

&lt;h3 id=&quot;the-short-answer-looking-at-bus-speeds-beforeafter-the-introduction-of-the-lanes-on-h-and-i-streets-its-not-clear-to-me-that-the-lanes-have-measurably-helped-improve-speeds&quot;&gt;The short answer: Looking at bus speeds before/after the introduction of the lanes on H and I streets, it’s not clear to me that the lanes have measurably helped improve speeds.&lt;/h3&gt;

&lt;p&gt;Read on to learn more about what I did to come to this conclusion.&lt;/p&gt;

&lt;h3 id=&quot;the-data&quot;&gt;The Data&lt;/h3&gt;
&lt;p&gt;Unlike DDOT (who have presumably been archiving their bus location data since the beginning of time, and should make their archive public), a friend has been archiving every real-time bus ping since March 2019 (&lt;a href=&quot;https://github.com/markongithub/bus_data_archive&quot;&gt;thanks, Mark!&lt;/a&gt;). The way this works is that every minute or so, any given bus will ping its latest location to a central server. Our archiving script runs on minute-ly intervals and catches those pings, saving them off into files which you can access &lt;a href=&quot;https://busdata-00-us-west-2.s3-us-west-2.amazonaws.com/&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this analysis, I look at data from March 2019 until December 31, 2019.&lt;/p&gt;

&lt;h3 id=&quot;the-methods&quot;&gt;The methods&lt;/h3&gt;
&lt;p&gt;In order to compute speeds on these routes, I performed the following procedures on the archived data.&lt;/p&gt;

&lt;p&gt;On each day’s file, I:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Filtered to just records that came from one of the 5 bus lines that run along H and/or I streets: the 30N, 30S, 32, 33, 36.&lt;/li&gt;
  &lt;li&gt;Each record has a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TripID&lt;/code&gt; associated with it that is unique to that particular trip the bus ran on that day. For each of those trips on that day, I pulled all records from that trip.&lt;/li&gt;
  &lt;li&gt;In each trip, pull out only those records that are either on H street or I street.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s what that looked like on Tuesday, 9/2/2019. Each color is a different trip.
&lt;img src=&quot;/static/images/h_street_gps_map.png&quot; alt=&quot;h street gps&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Then, we filter for trips on each corridor with at least 3 GPS pings (and no more than 10) within the corridor, and compute the average speed for those by taking the average rate between the last GPS point and first GPS point on the corridor. The hope is to see change over time and over all trips, so this might not be a perfect measure of speed, but if the buses start moving through this corridor faster, it should be able to pick up on that. Here’s what some of that processed data looks like from 2019-09-02, when there were 183 reported trips within the corridor that met the ‘3-10 GPS point’ criterion.&lt;/p&gt;

&lt;table border=&quot;1&quot; class=&quot;dataframe&quot; style=&quot;background-color: white; color: black;&quot;&gt;
  &lt;thead&gt;
    &lt;tr style=&quot;text-align: right;&quot;&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;TripID&lt;/th&gt;
      &lt;th&gt;distance&lt;/th&gt;
      &lt;th&gt;time&lt;/th&gt;
      &lt;th&gt;rate&lt;/th&gt;
      &lt;th&gt;corridor&lt;/th&gt;
      &lt;th&gt;n&lt;/th&gt;
      &lt;th&gt;start_t&lt;/th&gt;
      &lt;th&gt;end_t&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th&gt;124&lt;/th&gt;
      &lt;td&gt;991536010&lt;/td&gt;
      &lt;td&gt;1674.544503&lt;/td&gt;
      &lt;td&gt;139.0&lt;/td&gt;
      &lt;td&gt;12.047083&lt;/td&gt;
      &lt;td&gt;h&lt;/td&gt;
      &lt;td&gt;3.0&lt;/td&gt;
      &lt;td&gt;2019-09-02 13:31:23&lt;/td&gt;
      &lt;td&gt;2019-09-02 13:33:42&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;74&lt;/th&gt;
      &lt;td&gt;949148010&lt;/td&gt;
      &lt;td&gt;1925.530003&lt;/td&gt;
      &lt;td&gt;116.0&lt;/td&gt;
      &lt;td&gt;16.599397&lt;/td&gt;
      &lt;td&gt;h&lt;/td&gt;
      &lt;td&gt;3.0&lt;/td&gt;
      &lt;td&gt;2019-09-02 06:53:38&lt;/td&gt;
      &lt;td&gt;2019-09-02 06:55:34&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;88&lt;/th&gt;
      &lt;td&gt;991486010&lt;/td&gt;
      &lt;td&gt;1847.698704&lt;/td&gt;
      &lt;td&gt;137.0&lt;/td&gt;
      &lt;td&gt;13.486852&lt;/td&gt;
      &lt;td&gt;i&lt;/td&gt;
      &lt;td&gt;3.0&lt;/td&gt;
      &lt;td&gt;2019-09-02 12:21:33&lt;/td&gt;
      &lt;td&gt;2019-09-02 12:23:50&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;From here, I specifically look at the time when the bus lane rules were in place – from 7-9:30AM and 4-6:30PM on weekdays – and see what the average speed for all buses on that corridor was during those times each day.&lt;/p&gt;

&lt;p&gt;Here’s the code: &lt;a href=&quot;https://github.com/cmoscardi/dc_bus_stuff&quot;&gt;https://github.com/cmoscardi/dc_bus_stuff&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;aand-the-results&quot;&gt;Aand, the results?&lt;/h3&gt;
&lt;p&gt;At first, I was optimistic after seeing results that looked like this. This is average speeds during the AM rush hour before/after the bus lane was painted on.&lt;/p&gt;
&lt;table border=&quot;1&quot; class=&quot;dataframe&quot; style=&quot;background-color: white; color: black;&quot;&gt;
  &lt;thead&gt;
    &lt;tr style=&quot;text-align: right;&quot;&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;Before bus lane&lt;/th&gt;
      &lt;th&gt;After bus lane&lt;/th&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;&lt;/th&gt;
      &lt;th&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;th&gt;H street&lt;/th&gt;
      &lt;td&gt;11.7 MPH&lt;/td&gt;
      &lt;td&gt;12.9 MPH&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;th&gt;I street&lt;/th&gt;
      &lt;td&gt;11.8 MPH&lt;/td&gt;
      &lt;td&gt;12.7 MPH&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;However, the story gets more complicated when we look at average speeds by day.&lt;/p&gt;

&lt;h3 id=&quot;what-do-the-day-by-day-speeds-look-like&quot;&gt;What do the day-by-day speeds look like?&lt;/h3&gt;

&lt;p&gt;Really, the ambiguity here is in the following graphs. First, let’s look at H street. The below plots show average bus speeds on the corridor each day, both before and after the new lanes got painted.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/h_street_corridor_timeseries.png&quot; alt=&quot;slow buses&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Yikes. Let’s break this out a little bit. First, the AM rush hour is categorically faster than the PM rush hour – the lowest (smoothed) daily average speed on H street is 10.5 MPH. Then, in the PM we see average speeds range from about 5.5MPH slowest… to a max of roughly 11.5 MPH!&lt;/p&gt;

&lt;p&gt;However, what’s arguably more interesting here is the trends in both charts. In the first, we see that there is effectively no change in trend before the red dashed line and after it. In other words, the buses appear to be getting progressively faster on the mornings on H street, but it doesn’t look like the bus lanes have anything to do with it – they were already getting faster before the introduction of the bus lanes!And on the other side, during the PM rush hour, we see effectively no change. The precipitous dip in speed after the introduction of the bus lane is concerning, but that does even out by the end of the study period.&lt;/p&gt;

&lt;p&gt;We see similar behavior on I street, but reversed.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/i_street_corridor_timeseries.png&quot; alt=&quot;more slow buses&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Perhaps here, it is the case that during the PM rush hour, we see some marginal improvement – it may be the case that the trend begins increasing slightly faster. It’s hard to see without a basis for comparison.&lt;/p&gt;

&lt;h3 id=&quot;how-can-we-dig-deeper&quot;&gt;How can we dig deeper?&lt;/h3&gt;
&lt;p&gt;At this point, it would be interesting to perform a difference-in-difference experiment here. Specifically, find an area that’s similar to I street during the PM rush hour (maybe other segments of road that these bus lines share), but didn’t have a bus lane painted in. And then compare the trend of bus speeds in that area. Did those buses get similarly faster during this time, or did something “special” happen on I street (or H street) once the lane was installed?&lt;/p&gt;

&lt;h3 id=&quot;final-thoughts&quot;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;With that being said, looking at these results thus far, it’s really not clear that these bus lanes have helped the buses move faster through this corridor. Transit advocacy groups in the area &lt;a href=&quot;https://ggwash.org/view/73622/rush-hour-on-a-single-day-nearly-300-violations-in-the-new-h-and-i-bus-only-lanes&quot;&gt;have been frustrated&lt;/a&gt; by enforcement in the lanes – this could certainly be the underlying issue here.&lt;/p&gt;

&lt;p&gt;As DDOT begins to install bus lanes elsewhere in the city (woohoo, 14th st NW!), I think a similar analysis to this one can help shed light on how well these measures are actually working.&lt;/p&gt;

&lt;p&gt;Plus, on November 12, &lt;a href=&quot;https://www.washingtonpost.com/transportation/2019/09/20/dc-makes-h-i-streets-bus-lanes-permanent-all-day/&quot;&gt;the lanes on H and I streets became permanent&lt;/a&gt; and enforced 7AM-7PM! Looking at the results above, you can start to see improvement. Check back in a few months and I’ll put up some graphs of how well that has been working.&lt;/p&gt;
</description>
        <pubDate>Thu, 20 Feb 2020 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2020/02/20/dc-bus-lanes.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2020/02/20/dc-bus-lanes.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>The Second Avenue Subway and Property Values</title>
        <description>&lt;p&gt;This past Fall, I matriculated at &lt;a href=&quot;http://cusp.nyu.edu/&quot;&gt;NYU CUSP&lt;/a&gt;. I’ve worked on a few cool projects this past semester, and wanted to share a couple of them. This first one is analyzing the impact of the Second Avenue Subway’s opening on real estate prices in Yorkville. Before I begin, I want to give special shout-outs to the best teammates ever, Sarah Schoengold and Hao Xi, who were an integral part of putting this project together!&lt;/p&gt;

&lt;p&gt;The Second Avenue line’s impact on real estate prices is interesting for a few reasons. First, East Harlem was just rezoned - in part to prepare for the next extension of the Second Avenue Subway line. By understanding what happened in Yorkville, maybe we can better understand how property values will change in East Harlem. Personally, having lived in the neighborhood (at 116th and Lexington), I know that a new subway line will dramatically improve the area’s accessibility - which will, without a doubt, bring a lot of change. Starting simple by attempting to quantify real estate price changes is a first step to understanding that change.&lt;/p&gt;

&lt;p&gt;Second, quantifying the value increase brought about by the subway line is useful for potential &lt;a href=&quot;https://en.wikipedia.org/wiki/Value_capture&quot;&gt;value capture&lt;/a&gt; strategies for funding infrastructure projects. If the city understands how much money real estate owners will make from proximity to a new subway line, then the city can take a cut of that value increase for itself. Of course, this is only one step toward helping fund new transportation projects - but if something like the Second Avenue Subway could pay for itself, then maybe the Governor could funnel some of that money into the basic maintenance the subway system so &lt;a href=&quot;https://www.nytimes.com/2017/11/18/nyregion/new-york-subway-system-failure-delays.html&quot;&gt;desparately needs&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-data&quot;&gt;The Data&lt;/h2&gt;

&lt;p&gt;The data for this project is easily accessible:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Department of Finance &lt;a href=&quot;http://www1.nyc.gov/site/finance/taxes/property-rolling-sales-data.page&quot;&gt;sales data&lt;/a&gt;. This dataset records each property sale that happens in NYC, including residential, commercial, etc… - as well as condos, co-ops, and other apartments. We restricted our analysis to residential apartment sales.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www1.nyc.gov/site/planning/data-maps/open-data/dwn-pluto-mappluto.page&quot;&gt;PLUTO&lt;/a&gt;. This dataset contains basic geographic and zoning information about each tax lot in the city, among other pieces of information (such as when the building was built, renovated, etc.). We use this to get exactly how far each sold unit was from the subway line.&lt;/li&gt;
  &lt;li&gt;Zillow data, to get some meta-information about each property. &lt;a href=&quot;https://github.com/sebscho/CA_SAS/blob/master/Zillow%2BAPI%2BCalls%20(1).ipynb&quot;&gt;Sarah&lt;/a&gt; put together a Jupyter notebook that scrapes the API for us.&lt;/li&gt;
  &lt;li&gt;Some &lt;a href=&quot;https://www.baruch.cuny.edu/confluence/display/geoportal/NYC+Mass+Transit+Spatial+Layers&quot;&gt;subway GIS data&lt;/a&gt; to tie it all together. Hao did this work in ArcGIS, though it wouldn’t be hard to replicate in GeoPandas, my preferred geospatial analysis library.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We had two main issues:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;The DoF sales data is messy. For starters, it’s a bit inconsistent with the way it labels apartment numbers. Additionally, (I think) it includes things like deed transfers, e.g. when someone passes ownership of an apartment on to a family member, as low-valued sales.&lt;/li&gt;
  &lt;li&gt;Zillow is not free, and when attempting to pull down information on the apartments we were able to uncover in the DoF data, was inconsistent with what information we could get back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Working through all this, we were still able to get ~1700 sales records from the past ten years - January 2008 through October 2017. This left us with a dataset of sales with the following baseline features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sale date&lt;/li&gt;
  &lt;li&gt;Sale price&lt;/li&gt;
  &lt;li&gt;Distance to nearest Second Avenue Subway stop&lt;/li&gt;
  &lt;li&gt;Building year built / renovated&lt;/li&gt;
  &lt;li&gt;Number of bedrooms in apartment&lt;/li&gt;
&lt;/ul&gt;

&lt;div&gt;
  &lt;div style=&quot;width: 100%; float: left;&quot;&gt;
    &lt;img src=&quot;https://github.com/sebscho/CA_SAS/blob/master/Figures/distance_after.png?raw=true&quot; style=&quot;width: 30%;&quot; /&gt;
  &lt;/div&gt;
  &lt;div style=&quot;width: 100%; float: left; &quot;&gt;
    &lt;p&gt;
      This  image shows the area that we&apos;ll consider, along with the subway stop locations, and finally, the distance of each lot (where again, lot boundaries are given by PLUTO) to the nearest subway station.
    &lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;If you want to dive into the data loading and munging, it’s all &lt;a href=&quot;https://github.com/sebscho/CA_SAS&quot;&gt;on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-methods-experimental-design&quot;&gt;The methods: experimental design!&lt;/h2&gt;

&lt;p&gt;This may have been my favorite aspect of the project. I haven’t spent a ton of time digging through the nuances of experimental designs, so it was fun to learn more. I had exposure to the following, traditional, experimental setup, which we’ll build off of:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;A group of people is partitioned into “treatment” and “control” groups.&lt;/li&gt;
  &lt;li&gt;The “treatment” group receives some sort of “intervention” - think, a new drug to lower cholesterol - and some “outcome” is measured - think, cholesterol levels.&lt;/li&gt;
  &lt;li&gt;The “control” group does not receive the “intervention”, but they have the same “outcome” measured as for the treatment group. In a drug study, the control group would likely receive a placebo pill and instructions to measure their cholesterol in the same way as the treatment group - the idea being, there is absolutely no difference (in aggregate) between the control group and the treatment group, aside from the fact that the treatment group is taking pills that are specifically designed to lower cholesterol.&lt;/li&gt;
  &lt;li&gt;The change in measured outcome is compared between treatment and control group - typically as an aggregated value. In the case of the cholesterol drug, we’d compare the mean cholesterol levels before and after treatment, and see which group experienced a bigger change.&lt;/li&gt;
  &lt;li&gt;Because of the way we set up the treatment and control groups to be identical aside from the contents of the pill they are taking, if the treatment group had lower cholesterol, we can conclude that the drug itself had an impact on the treatment group (or did not).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is pretty much the experimental design you’ll learn in statistics 101 (that’s more-or-less where I first learned about it).&lt;/p&gt;

&lt;p&gt;In the social sciences, things are a little different. Setting up an “experiment” around a question like the impact of a subway line on real estate values is tough, because we can’t explicitly &lt;em&gt;control&lt;/em&gt; in the traditional sense (as in the drug study example above). We can’t separate out some apartments and deny those units access to the Second Avenue Subway, and then measure how their values are impacted compared to those that do have access. Instead, we have to be a little bit more clever. Our team’s digging came up with a few possible experimental designs.&lt;/p&gt;

&lt;h3 id=&quot;the-initial-method-difference-in-difference&quot;&gt;The initial method: Difference-in-Difference&lt;/h3&gt;
&lt;p&gt;One of the most popular for this sort of study is a &lt;a href=&quot;https://en.wikipedia.org/wiki/Difference_in_differences&quot;&gt;difference-in-differences&lt;/a&gt;. The idea with difference-in-differences is we want to set up something as close to an experiment as we possibly can, but with the observational data that we have. Thus, in our case, we’ll pick a “treatment” zone – an area we assume will be affected by the line’s opening, then try to find a “control” zone that has very similar characteristics aside from the fact that the Second Avenue Subway didn’t just open right next to it. Once we’ve done that, we can measure the average sale price before and after, in our treatment and control zones, and compare how big the change was between the two areas.&lt;/p&gt;
&lt;div&gt;
  &lt;div style=&quot;float: left; width: 30%;&quot;&gt;
    &lt;img src=&quot;https://github.com/sebscho/CA_SAS/blob/master/Figures/distance%20reduced.png?raw=true&quot; style=&quot;width: 90%; display: inline; margin-left: 20px;&quot; /&gt;
  &lt;/div&gt;
  &lt;div style=&quot;float: left; width: 40%;&quot;&gt;
    &lt;img src=&quot;/static/images/sas_did.png&quot; style=&quot;width: 100%;&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The above images visually explain the Difference-in-Differences setup. In the first, we can see both our choice of study area - roughly speaking, the Upper East Side - as well as our choice of treatment and control areas. Specifically, we consider all buildings east of Second Avenue (the highlighted area) to be members of the treatment zone, while the rest of the Upper East Side (the washed out area) is the “control zone.” By making this choice, we are saying that everything west of Second Avenue is close enough to the 4/5/6 line that its value was not impacted by the opening of the line. This may not be strictly true (e.g. the Second Avenue line is far more pleasant to ride at rush hour, from personal experience, and maybe it will help decrowd the 4/5/6 line), but we suspect it is a safe assumption to make.&lt;/p&gt;

&lt;p&gt;In the second image, we compare median prices east of Second Avenue (the blue “treatment” line) to those on the rest of the Upper East Side (orange “control” line). As we can see, the Upper East Side is categorically more expensive than Yorkville, though otherwise prices have varied similarly (albiet at different scales) over time. The black vertical line is January 1, 2017 - the day the Second Avenue line opened. Another way of thinking of this - the experimental way - is that it’s the day our “intervention” actually occurred.&lt;/p&gt;

&lt;p&gt;One of the nice things about Difference-in-Difference experiments is that they can be mathematically formulated as linear regressions - I won’t bore you with &lt;a href=&quot;https://en.wikipedia.org/wiki/Difference_in_differences#Implementation&quot;&gt;the details&lt;/a&gt;, but this means we can add in other factors to control for things that may differ between the two areas (or otherwise impact price).&lt;/p&gt;

&lt;h3 id=&quot;method-2-repeat-sales&quot;&gt;Method #2: repeat-sales&lt;/h3&gt;
&lt;p&gt;Another popular method is &lt;a href=&quot;https://www.investopedia.com/terms/r/repeatsales-method.asp?lgl=myfinance-layout-no-ads&quot;&gt;repeat-sales&lt;/a&gt;. The idea here is that rather than comparing sales and controlling for features that may affect sale price (e.g. apartment square footage), we can just look at the changes in sale price &lt;em&gt;of the same apartment&lt;/em&gt;. If we think about this as a regression, where we include factors that control for substantive features, by taking the delta, the substantive features drop out of the equation and we’re just left with time-based changes.&lt;/p&gt;

&lt;p&gt;As an example, let’s say we develop a linear model for housing price:&lt;/p&gt;

\[price = x \cdot bedrooms + y \cdot bathrooms + z \cdot sale\_year\]

&lt;p&gt;(caution! this is a bad model for a few reasons, but this is an example)&lt;/p&gt;

&lt;p&gt;And then, suppose we use this model to look at the sale of a 2br, 1bath apartment in 2007 and 2010:&lt;/p&gt;

\[price_{2007} = x \cdot 2 + y \cdot 1 + z \cdot 2007\]

\[price_{2010} = x \cdot 2 + y \cdot 1 + z \cdot 2010\]

&lt;p&gt;Now, if we want to look at the &lt;em&gt;change&lt;/em&gt;, we could subtract the 2010 price from the 2007 price. Doing so with the model gives us:&lt;/p&gt;

\[\delta_{price} = price_{2010} - price_{2007} 
= x \cdot (2 - 2) + y \cdot (1 - 1) + z \cdot (2010 - 2007)\]

\[\delta_{price} = z \cdot (2010 - 2007)\]

&lt;p&gt;Because we’re looking at the &lt;em&gt;exact same unit&lt;/em&gt;, only time-based features, such as the year in this case, remain in the model of the price growth. This greatly simplifies our assumptions.&lt;/p&gt;

&lt;p&gt;In our case, we looked at the “distance improvement to nearest subway” between the two sales. That is, if the first sale in a pair occurred &lt;em&gt;before&lt;/em&gt; the line opened and the second sale occurred &lt;em&gt;after&lt;/em&gt;, we took a look at how much shorter that apartment’s walking time to the subway was now that the Second Avenue line was open - as compared to their old walk to the 4/5/6 line. Using this method, areas on the far east side of Yorkville will see the greatest price jump, as they have the most dramatic distance improvements.&lt;/p&gt;

&lt;div&gt;
  &lt;div style=&quot;float: left; width: 40%;&quot;&gt;
    &lt;img src=&quot;https://github.com/sebscho/CA_SAS/blob/master/Figures/distance%20reduced.png?raw=true&quot; style=&quot;width: 100%;&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Looking back to this map, we can actually see the time-based variable we’ll care about in the repeat-sales approach. In particular, we can see a heatmap of how much each property improved its walking distance to the nearest subway when the Second Avenue Subway opened. Red is “most improved” (so as we can see, properties on the far east side of the island gained the most in terms of travel distance improvement). Interestingly, the “green” areas on the map are actually still closer to the nearest 4/5/6 train stop. (Note: ignore the washed out area - even though eveything in it is red, this is not being correctly plotted).&lt;/p&gt;

&lt;div&gt;
  &lt;div style=&quot;float: left; width: 60%; background-color: white;&quot;&gt;
    &lt;img src=&quot;/static/images/repeat_sales.png&quot; style=&quot;width: 90%; display: inline; margin-left: 20px;&quot; /&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This graph shows the change in distance on the X-axis, compared to the change in sale price on the Y-axis. Many properties in our area of interest experienced a change of 0 with the line’s opening, since they are already closer to the 4/5/6 line. However, there is a clear, though small in magnitude, upward linear trend for those properties which did get a walking distance improvement. We’ll quantify this improvement in our results below.&lt;/p&gt;

&lt;h3 id=&quot;for-the-future-regression-discontinuity&quot;&gt;For the future: regression discontinuity&lt;/h3&gt;
&lt;p&gt;There’s another interesting design that we didn’t try, but I think may be worth testing: &lt;a href=&quot;https://en.wikipedia.org/wiki/Regression_discontinuity_design&quot;&gt;Regression discontinuity&lt;/a&gt;. Let’s say that, for the sake of argument, we consider the area around Yorkville (the neighborhood that the Second Avenue Subway opened in) to be quite similar to Yorkville itself. So similar, in fact, that it’s going to be our control area. This experimental design would then look at prices on the margin of the “treatment” vs. “control” groups - for example, homes just west of Second Avenue, if we stick to the treatment/control area we chose. Another interesting break-point might be areas that are just &lt;em&gt;slightly&lt;/em&gt; closer to the new Second Avenue line than the 4/5/6, or &lt;em&gt;slightly&lt;/em&gt; farther - as the below map illustrates.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/static/images/reg_discon.png&quot; style=&quot;width: 40%;&quot; /&gt;
Apologies for the poor drawing.&lt;/p&gt;

&lt;h2 id=&quot;the-results&quot;&gt;The Results&lt;/h2&gt;

&lt;h3 id=&quot;difference-in-difference&quot;&gt;Difference-in-Difference&lt;/h3&gt;
&lt;p&gt;It appears that we have a bit of work to do on the DID model. In particular, similar studies tend to report their models’ performance by their R^2, essentialy the amount of variance in the data explained by the model. Our model had an R^2 of ~.6 while others had far higher R^2 values (close to .8 and even as high as .9). I suspect we may need to further segment the market - for example, maybe we need to control for whether the sold unit is a co-op or a condo. With that said, the DID coefficient of .14 is high - implying that on average, sales east of 2nd Avenue and after January 1, 2017 average about 14% higher than an equivalent sale outside of that treatment zone, holding all other things constant. Wow - that’s a lot!&lt;/p&gt;

&lt;p&gt;Something else we tried to do was calculate anticipation effects - basically, understand how much prices would go up in &lt;em&gt;expectance&lt;/em&gt; of the line’s opening, rather than the actual opening itself. To do this, we essentially set another “cutoff” point 6 months in advance of the line’s opening to see how much that sales increased before vs. after that point. Unfortunately, due to limitations with the Zillow API, we had trouble doing this - we weren’t able to pull both information on apartments sold right before and right after the line’s opening, so we opted for sales right after the line’s opening.&lt;/p&gt;

&lt;h3 id=&quot;repeat-sales&quot;&gt;Repeat-sales&lt;/h3&gt;
&lt;p&gt;We saw a statistically significant indicator that the SAS has increased property values. In particular, for every 100m of walkability improvement a given unit gained with the SAS opening, its growth factor increased 2% on average. This translates to 8% above-average growth for the units on the far east side of Yorkville, which got a ~400m walkability improvement to the nearest subway. So according to this model, a unit that would have sold for \$400,000 at 86th and York is now worth ~$432,000 … just by virtue of this subway line opening!*&lt;/p&gt;

&lt;p&gt;With that said, we had some methodological issues. In theory, with repeat-sales, we should be able to account for the variation to a high degree. A &lt;a href=&quot;https://corpus.ulaval.ca/jspui/handle/20.500.11794/2928&quot;&gt;similar study&lt;/a&gt; in Montreal reported R^2 values of .7 for a baseline model - ours were significantly lower. I’m not sure why this is - perhaps different sized apartments are fundamentally different markets in New York, and we can’t consider studio apartments in conjunction with 2-bedroom apartments, for example.&lt;/p&gt;

&lt;p&gt;* Note that our model is predicting the difference in &lt;em&gt;log&lt;/em&gt; prices here - which can also be written as a ratio. So, if a house’s resale price grew $x$ percent, then the ratio of $p_{new} / p_{old} = 1 + (x / 100) $. So, with 8% growth and taking the log, as it turns out, log(1.08) ~ .08. This works more generally with growth factors &amp;lt; 25% (log(1.25) ~ .22) , so we’re fairly safe to approximate here.&lt;/p&gt;

&lt;h2 id=&quot;the-conclusion&quot;&gt;The Conclusion&lt;/h2&gt;
&lt;p&gt;You don’t have to be a real-estate professional to know that the Second Avenue Subway has jacked up values in Yorkville - and you didn’t have to be an industry professional to have guessed that would happen before the line opened. What’s interesting here is that we’ve attempted to quantify exactly how much transit accessibility improves property values - and the answer is that it improves them quite substantially. We can use this information to better fund, plan, and develop new infrastructure around the city, especially as the city begins to develop the next phase of the Second Avenue line’s opening. We hope it may be useful for policymakers to better understand the implications of these infrastructure decisions.&lt;/p&gt;

&lt;h3 id=&quot;but-what-about-renting&quot;&gt;But what about renting?&lt;/h3&gt;
&lt;p&gt;Something we haven’t looked into, but are very interested in, is rental prices. Do rent increases behave differently than sale price increases? Are there anticipation effects? Do we see rents increase more immediately due to the shorter overall timescale of renting? Unfortunately, rental data is hard to acquire, but given access to the data, we would be very interested in researching this.&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you want to learn more, there’s &lt;a href=&quot;https://github.com/sebscho/CA_SAS/blob/master/analysis.ipynb&quot;&gt;a notebook&lt;/a&gt; where all of this analysis is captured.&lt;/p&gt;
</description>
        <pubDate>Mon, 18 Dec 2017 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2017/12/18/SAS-real-estate-values.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2017/12/18/SAS-real-estate-values.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Anaconda Dependency Minification</title>
        <description>&lt;p&gt;At &lt;a href=&quot;https://www.thedataincubator.com&quot;&gt;The Data Incubator&lt;/a&gt;, we really like anaconda. In particular, we like that we can quickly install and update environments that include the usual PyData libraries (numpy, scipy, pandas) at any version from binary code. One issue we’ve had, however, is that freezing conda environments, currently, is machine-specific. In particular, there are subtle differences in versions of binary dependencies required on different machines for cairo, a dependency of matplotlib.&lt;/p&gt;

&lt;p&gt;However, we (and, we suspect, most data scientists) don’t really care about that. We just want PyData at the appropriate versions on our machines. We tried to capture this behavior by minifying the environment.yml file and taking only the “most important” packages - the ones that, because they depend on other packages, will install everyting currently specified in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment.yml&lt;/code&gt;. For the rest of the packages, we just want the latest version, and anaconda automatically behaves this way for us.&lt;/p&gt;

&lt;h2 id=&quot;dependencies-and-graphing-them&quot;&gt;Dependencies and graphing them&lt;/h2&gt;
&lt;p&gt;So, maybe you’ve thought about dependencies before. We have. As it turns out, we can think of dependencies as a directed acyclic graph (often called by their abbreviation, DAG).* In this graph, each package is a node, and if package X depends on package Y, we have a (directed) edge (X, Y).&lt;/p&gt;

&lt;p&gt;If you look at such a graph, you’ll find nodes that represent packages which aren’t dependencies for any other packages? We call those “source nodes” (or source packages, for the purposes of this explanation). Those are the nodes we care about, as they represent the packages we really care about. To find this set of packages, we can take the complement (in graph theory parlance, “the cut”) of the subset of all nodes which are specified as another package’s dependency. This leaves us with our source packages.&lt;/p&gt;

&lt;p&gt;* We could conceivably have two packages which are “tightly coupled” - i.e. depend on each other - but that’s typically considered pretty bad practice. We know of no examples of this in anaconda’s repositories.&lt;/p&gt;

&lt;h2 id=&quot;code&quot;&gt;Code&lt;/h2&gt;
&lt;p&gt;First, without further ado, here is the package itself:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;https://github.com/thedataincubator/conda_minifier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, if you want to just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt;…&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pip install git+https://github.com/thedataincubator/conda_minifier.git@master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, usage is quite simple.&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python -m conda_minifier path_to_environment.yml &amp;gt; minified_environment.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now that you’ve got the goods, here is a bit more about how we do this in practice.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;Parse all packages/versions in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;environment.yml&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Get the dependencies of each package and store them in a set (so we know which packages appeared as a dependency at least once).&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We do this with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;conda info [package_name]&lt;/code&gt;. Here’s a sample output:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;cmoscardi.github.io]&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;conda info numpy
Fetching package metadata: ....

numpy 1.5.1 py27_pro0
&lt;span class=&quot;nt&quot;&gt;---------------------&lt;/span&gt;
file name   : numpy-1.5.1-py27_pro0.tar.bz2
name        : numpy
version     : 1.5.1
build number: 0
build string: py27_pro0
channel     : defaults
size        : 3.4 MB
build_target: pro
&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;        : 2012-11-09
features    : mkl
license     : BSD
md5         : 7379c7071ed6c29568a4bb7ca22efc57
installed environments:
dependencies:
    mkl 10.3
    nose
    python 2.7&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;

numpy 1.5.1 py27_ce0
&lt;span class=&quot;nt&quot;&gt;--------------------&lt;/span&gt;
file name   : numpy-1.5.1-py27_ce0.tar.bz2
name        : numpy
version     : 1.5.1
build number: 0
build string: py27_ce0
channel     : defaults
size        : 4.6 MB
build_target: ce
&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;        : 2012-11-09
license     : BSD
md5         : f53b0807eac1e45910dd2ef5e1da4b97
installed environments:
dependencies:
    nose
    python 2.7&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;


.....&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As you can see in the source code, we chose to use python to scrape this output.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Take the complement of all requirements and every package that appears as a dependency, making sure to preserve version for these “important” packages.
It’s a simple snippet:&lt;/li&gt;
&lt;/ol&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;calc_difference&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deps&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
  packages: a dict where keys are package names, values are versions (or None).
  deps: the set of all package names which occurred at least once as a dependency.
  &quot;&quot;&quot;&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;source_names&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deps&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;source_names&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And that’s it! We have a minified environment. Simple, right?&lt;/p&gt;

&lt;h1 id=&quot;one-other-thing&quot;&gt;One other thing&lt;/h1&gt;
&lt;p&gt;It’s worth noting that our minifier also removes the “binary” package spec at the end of the version string. For example, OSX and Linux may have different binaries compiled for each respective platform.
We remove that platform-specific bit, and make conda resolve depending on the machine we’re on.&lt;/p&gt;

&lt;h1 id=&quot;pitfalls--gotchas&quot;&gt;Pitfalls / Gotchas&lt;/h1&gt;
&lt;p&gt;We’ve come across some interesting cases. For example, what if we want to include Bokeh, which depends on numpy, scipy, and pandas? Suddenly, we have to explicitly declare our PyData packages, since otherwise our “minimum set” non-transitive dependencies is just Bokeh. Since pandas depends on numpy and scipy, even including pandas means we don’t specify our numpy or scipy versions. Our suggestion: if you know you need numpy 1.9.2, hard-specify that version after running this minifier.&lt;/p&gt;
</description>
        <pubDate>Wed, 18 May 2016 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2016/05/18/dependency-minification-in-python.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2016/05/18/dependency-minification-in-python.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Testing Jupyter Notebooks</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://jupyter.org/&quot;&gt;Jupyter&lt;/a&gt; is a fantastic tool that we use at &lt;a href=&quot;http://www.thedataincubator.com/&quot;&gt;The Data Incubator&lt;/a&gt; for instructional purposes. One perk of using Jupyter is that we can easily test our code samples across any language there’s a Jupyter kernel for. In this post, I’ll show you some of the code we use to test notebooks!&lt;/p&gt;

&lt;p&gt;First, a quick discussion of the current state of testing ipython notebooks: there isn’t much documented about the process. &lt;a href=&quot;https://github.com/taavi/ipython_nose&quot;&gt;ipython_nose&lt;/a&gt; is a really helpful extension for writing tests into your notebooks, but there’s no documentation or information about easy end-to-end testing. In particular, we want the programmatic equivalent of clicking “run all cells”.&lt;/p&gt;

&lt;p&gt;After poking around things like &lt;a href=&quot;https://github.com/ipython/ipython/wiki/A-gallery-of-interesting-IPython-Notebooks&quot;&gt;github’s list of cool ipython notebooks&lt;/a&gt; and the Jupyter docs, two things became apparent to us:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Most people do not test their notebooks.&lt;/li&gt;
  &lt;li&gt;Automated end-to-end testing is extremely easy to implement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Given 2. , 1. is really surprising. Having automated testing makes any developer’s life &lt;em&gt;so much easier&lt;/em&gt; - and that includes curriculum devs, instructors, or individuals writing technical talks that present lecture notes in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.ipynb&lt;/code&gt; format. Hence this post: hopefully this post will help make at least a few people’s lives easier!&lt;/p&gt;

&lt;p&gt;In terms of testing code execution, here’s what our testing process looks like at a high level. It’s actually really simple and basically mirrors the requirements we described above.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Execute notebook.&lt;/li&gt;
  &lt;li&gt;Check output cells for errors and raise the first one we find.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(We also test other things in our notebooks - e.g. that all of our links are up-to-date)&lt;/p&gt;

&lt;h2 id=&quot;first-step-execute-notebook&quot;&gt;First step: Execute notebook&lt;/h2&gt;

&lt;p&gt;As it turns out, programmatic execution of Jupyter notebooks is pretty straightforward. You can run the command:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;jupyter nbconvert &lt;span class=&quot;nt&quot;&gt;--to&lt;/span&gt; notebook &lt;span class=&quot;nt&quot;&gt;--execute&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--ExecutePreprocessor&lt;/span&gt;.timeout&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;60&lt;span class=&quot;se&quot;&gt;\ &lt;/span&gt;
                  &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt; out_file in_file&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And end up with a fully executed notebook. As mention above, we run a few other tests on our notebooks - and those tests are written in python (this partially has to do with the fact that Jupyter used to be IPython Notebook). As such, we also wrote a wrapper function to run this code in python and return the parsed notebook as a python object.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;os&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;subprocess&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;tempfile&lt;/span&gt;

&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;nbformat&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_notebook_run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;Execute a notebook via nbconvert and collect output.
       :returns (parsed nb object, execution errors)
    &quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;os&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chdir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;dirname&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;tempfile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;suffix&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;.ipynb&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;nbconvert&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;--to&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;notebook&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;--execute&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;&quot;--ExecutePreprocessor.timeout=60&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;s&quot;&gt;&quot;--output&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;subprocess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;check_call&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;fout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;nb&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nbformat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;fout&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nbformat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_nbformat&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cells&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;outputs&quot;&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt;
                     &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cell&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;outputs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;\
                     &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;output_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h2 id=&quot;next-step-checking-output-cells&quot;&gt;Next Step: Checking Output Cells&lt;/h2&gt;
&lt;p&gt;This is the part you can do with any test suite you want. We want to confirm that the notebook in question runs all cells correctly.&lt;/p&gt;

&lt;p&gt;In python, using a framework like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;py.test&lt;/code&gt;, we can do something as simple as this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;test_ipynb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;():&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;nb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_notebook_run&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;my_notebook.ipynb&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And receive back the specific errors that occurred when trying to execute our cells.&lt;/p&gt;

&lt;h2 id=&quot;thats-it--here-are-some-other-things-you-could-do-to-ensure-production-quality-notebooks&quot;&gt;That’s it!  Here are some other things you could do to ensure production-quality notebooks:&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;(Python only) Use the &lt;a href=&quot;https://github.com/taavi/ipython_nose&quot;&gt;ipython_nose&lt;/a&gt; extension to write unit tests for your code. If you include them at the bottom of the notebook, they’ll be executed as normal cells, and will raise errors if a test fails! This way you can check, for example, interactive functionality as users begin playing with your notebook.&lt;/li&gt;
  &lt;li&gt;Lint your code cells using something like &lt;a href=&quot;https://pypi.python.org/pypi/flake8&quot;&gt;flake8&lt;/a&gt; for your favorite language.&lt;/li&gt;
  &lt;li&gt;Check that all links in your notebooks are alive.&lt;/li&gt;
&lt;/ul&gt;
</description>
        <pubDate>Wed, 20 Jan 2016 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2016/01/20/jupyter-testing.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2016/01/20/jupyter-testing.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Chef and Google App Engine</title>
        <description>&lt;p&gt;Anyone who’s ever tried to write a nontrivial application on Google App Engine has encountered at least seven* design decisions that have led to serious head-scratching moments. One of those happened to me about a month ago, while integrating &lt;a href=&quot;https://www.chef.io/chef/&quot;&gt;Chef&lt;/a&gt; into our course at &lt;a href=&quot;www.thedataincubator.com&quot;&gt;The Data Incubator&lt;/a&gt;. Our goal was to allow for one-click spinning up (on &lt;a href=&quot;http://digitalocean.com&quot;&gt;DigitalOcean&lt;/a&gt;’s cloud) and monitoring of our Fellows’ course machines, already under Chef management.&lt;/p&gt;

&lt;p&gt;* No basis in fact - there are probably more than seven. It should be noted that the Google Cloud Platform is going to greatly improve this situation by allowing you to deploy Docker containers - woohoo!&lt;/p&gt;

&lt;p&gt;##A First Look&lt;/p&gt;

&lt;p&gt;Chef servers have an &lt;a href=&quot;https://docs.chef.io/api_chef_server.html&quot;&gt;HTTP API&lt;/a&gt;. Seems like it’d be an easy integration, right? While GAE doesn’t let you do many things (including making SMTP connections), one thing you, thankfully, &lt;em&gt;can&lt;/em&gt; do with relative ease is make HTTP requests (although everyone’s favorite python HTTP library, &lt;a href=&quot;http://www.python-requests.org/en/latest/&quot;&gt;requests&lt;/a&gt;, is a &lt;a href=&quot;http://stackoverflow.com/questions/21605328/python-requests-on-google-app-engine-not-working-for-https&quot;&gt;total&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/9762685/using-the-requests-python-library-in-google-app-engine&quot;&gt;nightmare&lt;/a&gt; - but that’s for another blogpost). This was going to be a quick job - we’d spend a couple days coding, write some tests, and have one-click deployment, right? &lt;em&gt;right?&lt;/em&gt;. As you probably guessed, that timeline was anything but right.&lt;/p&gt;

&lt;h2 id=&quot;problem-1--pycrypto-and-pycrypto-only&quot;&gt;Problem #1 : PyCrypto and PyCrypto only.&lt;/h2&gt;
&lt;p&gt;Not only does Chef have an HTTP API, but there’s a &lt;a href=&quot;https://github.com/coderanger/pychef&quot;&gt;pretty good library&lt;/a&gt; that wraps it in user-friendly python. Unfortunately, that library uses &lt;a href=&quot;https://github.com/coderanger/pychef/blob/master/chef/rsa.py&quot;&gt;libcrypto&lt;/a&gt;, meaning that it’s a no-go for GAE.&lt;/p&gt;

&lt;p&gt;Why, do you ask, is Chef’s API requiring the use of libcrypto? Well, let’s examine that Chef server &lt;a href=&quot;https://docs.chef.io/api_chef_server.html&quot;&gt;HTTP API&lt;/a&gt; doc again. You’ll notice you have to send very specific requests, with very specificially encoded headers, to the Chef Server. In particular, you need to use a Chef Client private key to sign the request (using an RSA signature protocol - private key signing) and send that signature in the headers.&lt;/p&gt;

&lt;p&gt;To circumvent this without getting unnecessarily deep into RSA implementation specifics, we looked at the &lt;a href=&quot;https://docs.chef.io/auth.html&quot;&gt;Chef docs&lt;/a&gt; and found some simple commands to sign the headers using &lt;a href=&quot;https://www.openssl.org/docs/manmaster/apps/rsautl.html&quot;&gt;openssl&lt;/a&gt;. We found that PyCrypto’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA.decrypt&lt;/code&gt;* function would appropriately generate our message signature, provided we byte-padded the input string beforehand (in accordance with &lt;a href=&quot;https://en.wikipedia.org/wiki/PKCS_1&quot;&gt;a particular set of standards&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;*A brief aside: we didn’t just stumble upon &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA.decrypt&lt;/code&gt; as the function to use… we had a bit of prior knowledge. RSA private key signatures work essentially by having the signer raise a message (or, more accurately, the message’s hash) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d&lt;/code&gt; power (where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d&lt;/code&gt; is the private key), allowing anyone with the public key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e&lt;/code&gt; to verify that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m^ed == m&lt;/code&gt; (mod n of course). RSA encryption also raises a message &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&apos;&lt;/code&gt; to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d&lt;/code&gt; power when the recipient wants to read the message using their private key, where &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;m&apos; == m ^ e&lt;/code&gt;. Anyway, enough math and protocols!&lt;/p&gt;

&lt;p&gt;Here’s what that byte-padding looks like (props to &lt;a href=&quot;http://engineering.hearsaysocial.com/2012/01/25/using-pycrypto-instead-of-m2crypto-on-google-app-engine/&quot;&gt;this post&lt;/a&gt; for the snippet, replicated in our own codebase):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;_emsa_pkcs1_v1_5_encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;em_len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
    em_len: # of bytes in the key
    m : message
    &quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\xff&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;em_len&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x00\x01&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ps&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\x00&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;With this out of the way, we thought we were in the clear, until…&lt;/p&gt;

&lt;p&gt;##Problem #2: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev_appserver.py&lt;/code&gt; doesn’t respect ignoring SSL verification.
Our Chef server lives on a DigitalOcean box, and the SSL certificate is self-signed (we dont need client-facing trustworthiness on this one). No problem: we’d just pass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify=False&lt;/code&gt; as a flag into our HTTP request code. Unfortunately, &lt;a href=&quot;http://stackoverflow.com/questions/28866770/appengine-urlfetch-validate-certificate-false-none-not-being-respected&quot;&gt;this&lt;/a&gt; recently became a problem in the GAE development environment.&lt;/p&gt;

&lt;p&gt;We still haven’t found a great solution to this, so we just ensure that locally, we have our self-signed certificate added into OpenSSL a la the following &lt;a href=&quot;http://unix.stackexchange.com/questions/90450/adding-a-self-signed-certificate-to-the-trusted-list&quot;&gt;post&lt;/a&gt;. If you have a better solution, please let me know!&lt;/p&gt;

&lt;h2 id=&quot;solution-an-open-source-library&quot;&gt;Solution: An Open Source Library!&lt;/h2&gt;
&lt;p&gt;We open-sourced the code we use to hopefully save other devs this headache. It uses version &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2.1.0&lt;/code&gt; of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;requests&lt;/code&gt; (the last version that &lt;a href=&quot;http://stackoverflow.com/questions/21605328/python-requests-on-google-app-engine-not-working-for-https&quot;&gt;can be used&lt;/a&gt; on GAE), and the latest PyCrypto (2.6.1 as of this writing). It works swimmingly, though only on the actual GAE environment if you don’t trust your Chef Server’s SSL certificate.&lt;/p&gt;

&lt;p&gt;Check it out &lt;a href=&quot;https://github.com/thedataincubator/chef_gae&quot;&gt;on github&lt;/a&gt; - you can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install chef_gae&lt;/code&gt; as well. Pull requests always welcome!&lt;/p&gt;
</description>
        <pubDate>Sat, 10 Oct 2015 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2015/10/10/chef-and-google-appengine.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2015/10/10/chef-and-google-appengine.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Embedding D3 in IPython Notebook</title>
        <description>&lt;p&gt;&lt;a href=&quot;https://jupyter.org/&quot;&gt;Jupyter&lt;/a&gt; is a fantastic tool that we use at &lt;a href=&quot;http://www.thedataincubator.com/&quot;&gt;The Data Incubator&lt;/a&gt; for instructional purposes. 
In particular, we like to keep our curriculum compartmentalized via Jupyter notebooks. It allows us to test our code samples across any language there’s a Jupyter kernel for* and keep things in one place, so our fellows don’t have to rifle through a wide variety of file formats before getting to the information they need.&lt;/p&gt;

&lt;p&gt;One area where we only recently integrated Jupyter was frontend web visualization. Our previous structure involved a notebook, possibly with code snippets, that contained links to various HTML files. We expected our fellows to dig through the code to&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Look at the HTML source for the basic layout.&lt;/li&gt;
  &lt;li&gt;Expose the Javascript powering the visualization.&lt;/li&gt;
  &lt;li&gt;View the styles making everything pretty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Oh, and any data processing code was separate/output to a file. Obviously not ideal. We knew IPython had a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%%javascript&lt;/code&gt; magic, and started rifling around to see what we could improve.&lt;/p&gt;

&lt;h1 id=&quot;including-d3js&quot;&gt;Including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d3.js&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;Conveniently, Jupyter already uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;require.js&lt;/code&gt;, and it works great! Thanks to &lt;a href=&quot;http://www.machinalis.com/blog/embedding-interactive-charts-on-an-ipython-nb/&quot;&gt;this blogpost&lt;/a&gt; (explaining a slightly more cumbersome way to embed D3) for the tipoff.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;o&quot;&gt;%%&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;javascript&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;paths&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;d3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;h1 id=&quot;the-big-discovery-element&quot;&gt;The Big Discovery: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;IPython’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%%javascript&lt;/code&gt; magic runs code client-side and sets a global JQuery-selected variable, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;element&lt;/code&gt;, to refer to the output cell. This is obviously very convenient - we now have a way to create arbitrary DOM elements in our cell. In particular, we can create SVG canvases and add SVG shapes to that canvas… see where this is going?&lt;/p&gt;

&lt;p&gt;This means we can write code like this:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;o&quot;&gt;%%&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;javascript&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;element&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;lt;div id=&apos;chart1&apos;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;and we’ve created a nice div in our output!&lt;/p&gt;

&lt;h1 id=&quot;data-conversion-pandas-dataframe---json&quot;&gt;Data Conversion: Pandas Dataframe -&amp;gt; JSON&lt;/h1&gt;
&lt;p&gt;This one’s a bit of a hack. Basically, since the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%javascript&lt;/code&gt; magic is run client-side, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window&lt;/code&gt; is set. So we do what every JS developer has (maybe shamefully) done at some point in their career, and bind data to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window&lt;/code&gt; so that it’s globally accessible.&lt;/p&gt;

&lt;p&gt;But wait, it gets better: Pandas dataframe objects have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_json&lt;/code&gt; function! The only trick now is managing to execute some JS code that loads the JSON dump we can get for free from Pandas. Here’s a snippet that does just that, invoking some of IPython’s backend display logic:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-python&quot; data-lang=&quot;python&quot;&gt;&lt;span class=&quot;kn&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;nn&quot;&gt;IPython.display&lt;/span&gt; &lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Javascript&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;#runs arbitrary javascript, client-side
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Javascript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&quot;&quot;
           window.vizObj={};
           &quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;df&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_json&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()))&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;As it turns out, Pandas dumps its dataframes in a way that isn’t exactly what D3 is &lt;a href=&quot;https://github.com/mbostock/d3/wiki/Selections#data&quot;&gt;looking for&lt;/a&gt; by default. You may want to restructure your data in a certain way for D3 - you now have the freedom to do that in JS or Python - as long as you can call &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;json.dumps&lt;/code&gt; on it in Python, you can bind it to D3. You could also call pandas’ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;to_csv&lt;/code&gt;, bind that string to the client side, and load it into D3 using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;d3.csv&lt;/code&gt; convenience function.&lt;/p&gt;

&lt;h1 id=&quot;final-result&quot;&gt;Final Result&lt;/h1&gt;
&lt;p&gt;With all this taken care of, there isn’t really much else to do! We now have interactive visualizations! After this, it’s up to you to write whatever you want in D3. This sort of embedding might be useful if you want to pass around analysis and visualization source code all-in-one, so that a collaborator can immediately reproduce a given result and help tweak your visualizations. If you’re giving a talk, it’s very useful for instructive purposes!&lt;/p&gt;

&lt;p&gt;Here’s a sample, very basic, D3 visualization. You can see the ipython notebook &lt;a href=&quot;https://github.com/cmoscardi/embedded_d3_example/blob/master/Embedded_D3.ipynb&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, github doesn’t render JS (how cool would that be…), so you can clone the repo to play with it yourself. But that notebook gives the gist of it.&lt;/p&gt;

&lt;h1 id=&quot;other-useful-tools&quot;&gt;Other useful tools&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;http://mpld3.github.io/&quot;&gt;mpld3&lt;/a&gt; - cross-compiling matplotlib into D3.js code, plays nice with ipython notebooks.&lt;/p&gt;

&lt;p&gt;*if you’re interested in Jupyter notebook testing, let me know - seems like good fodder for another post!&lt;/p&gt;
</description>
        <pubDate>Wed, 12 Aug 2015 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2015/08/12/embedding-d3-in-ipython-notebook.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2015/08/12/embedding-d3-in-ipython-notebook.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
      <item>
        <title>Hello world!</title>
        <description>&lt;p&gt;This one’s been a long time coming. I now have a blog. Woohoo!&lt;/p&gt;

&lt;p&gt;I will post about a variety of things. One of my favorite blog-functions is a repository for random bits of knowledge you wouldn’t keep track of otherwise. I have a good feeling that’s what you’ll mostly find here - stuff that’s not just germane, but also useful, to my interests - hopefully it’s at least interesting, if not useful, to you as well!&lt;/p&gt;
</description>
        <pubDate>Tue, 11 Aug 2015 17:20:40 +0000</pubDate>
        <link>https://www.christianmoscardi.com/blog/2015/08/11/hello-world.html</link>
        <guid isPermaLink="true">https://www.christianmoscardi.com/blog/2015/08/11/hello-world.html</guid>
        
        
        <category>blog</category>
        
      </item>
    
  </channel>
</rss>
