<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Archive Fever by Edwin Wenink</title><link>https://www.edwinwenink.xyz/</link><description>Recent content on Archive Fever by Edwin Wenink</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Fri, 15 Mar 2024 12:21:46 +0100</lastBuildDate><atom:link href="https://www.edwinwenink.xyz/index.xml" rel="self" type="application/rss+xml"/><item><title>Idiot's guide to resource migration in Terraform</title><link>https://www.edwinwenink.xyz/posts/73-terraform_migration_guide/</link><pubDate>Fri, 15 Mar 2024 12:21:46 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/73-terraform_migration_guide/</guid><description>&lt;p&gt;Sometimes we need to migrate resources that are managed in Terraform.
Terraform is a declarative language to manage cloud infrastructure from code, which allows you to reliably automate your deployments and put your infrastructure configuration under version control.
We call this Infrastructure as Code (IaC).&lt;/p&gt;
&lt;p&gt;When moving resources in your IaC, Terraform will by default delete the resource and re-create it at the new path.
In the best case this is relatively harmless, depending on whether some downtime is acceptable, but nevertheless wasted effort because we already know the recreated resource will be identical to the removed resource.
Worst case, we absolutely do not want to delete and recreate the resource, for example because it holds data (think: storage accounts, databases).
Another reason you do not want to recreate resources is if they use on system-assigned identities for connecting to other resources (role-based access that is tied to the lifecycle of the resource itself).
In that case, recreating the resource will likely break functionality.&lt;/p&gt;
&lt;p&gt;So how do we &lt;em&gt;properly&lt;/em&gt; migrate cloud resources managed by Terraform?
Properly means that Terraform will update its pointer to the remote resource and that the remote resource itself remains unaffected.
There are various scenarios in which you need to migrate Terraform resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A. Moving resource blocks within the same Terraform state, for example to a child module.&lt;/li&gt;
&lt;li&gt;B. Moving a resource block to another module altogether, for example because over time you have developed dependency cycles and refactoring is overdue.&lt;/li&gt;
&lt;li&gt;C. Removing a resource from Terraform management altogether without deleting the resource.&lt;/li&gt;
&lt;li&gt;D. Bring an existing resource under Terraform management without Terraform trying to create a resource that already exists&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Scenario A and B are the most interesting scenarios.
In particular scenario B is a superset of scenarios C and D, so they are discussed in one go.&lt;/p&gt;
&lt;h2 id="scenario-a-moving-resources-within-the-same-terraform-state"&gt;Scenario A: moving resources within the same Terraform state &lt;a href="#scenario-a-moving-resources-within-the-same-terraform-state"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Make the refactor in your IaC&lt;/li&gt;
&lt;li&gt;In a Terraform plan, observe which resources are deleted and then recreated at the new path&lt;/li&gt;
&lt;li&gt;For each resource, define a &lt;code&gt;moved&lt;/code&gt; block.
&lt;ul&gt;
&lt;li&gt;You can do this in a separate file, or keep the &lt;code&gt;moved&lt;/code&gt; blocks next to the moved resource.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Verify that in the final Terraform plan there are no creations and deletions. If there are in-place updates, verify that they are intended.
&lt;ul&gt;
&lt;li&gt;For example, if you accidentally import the right resource but from the wrong subscription (for example your staging environment instead of your dev environment) this may show up as an in-place update, depending on if that particular field forces replacement for that given resource.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After moving, it is optional to keep the move statements around.
I tend to keep them around until everything has been working great for a while, because the &lt;code&gt;moved&lt;/code&gt; blocks are essentially an explicit history of how you manipulated the Terraform state.&lt;/p&gt;
&lt;h2 id="scenario-b-moving-a-resource-to-another-terraform-state"&gt;Scenario B: moving a resource to another Terraform state &lt;a href="#scenario-b-moving-a-resource-to-another-terraform-state"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This scenario is a lot more tricky!
This is because a failure to correctly remove blocks from the Terraform state will &lt;em&gt;trigger deletion&lt;/em&gt; of those resources.&lt;/p&gt;
&lt;p&gt;Always carefully inspect your plans before applying anything.&lt;/p&gt;
&lt;p&gt;In this example, we will move one resource from module A to module B.
Module A and B each have their own backend and Terraform state.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Remove the resource from the IaC of module A&lt;/li&gt;
&lt;li&gt;Record this removal with a &lt;code&gt;removed&lt;/code&gt; block (available since Terraform 1.7+ )&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Prior to Terraform 1.7, this requires you to manually run &lt;code&gt;terraform state rm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Include the resource in the IaC of module B&lt;/li&gt;
&lt;li&gt;Record an &lt;code&gt;import&lt;/code&gt; block that maps the existing resource to the new location
block that maps the existing resource to the new location&lt;/li&gt;
&lt;li&gt;Ensure that the Terraform plan for &lt;em&gt;both modules&lt;/em&gt; do not include deletions and creations. Carefully inspect possible in-place updates and ensure they are innocent.
&lt;ul&gt;
&lt;li&gt;An innocent example of an in-place update is capitalization in a resource group name if the resource name is case insensitive.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="what-can-go-wrong"&gt;What can go wrong? &lt;a href="#what-can-go-wrong"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id="importing-without-removing"&gt;Importing without removing &lt;a href="#importing-without-removing"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;A lot of extra complexity arises from the fact that during your refactor, your teammates will probably happily keep on working in another branch.
During the migration, it is absolutely necessary to communicate this with teammates and do not make any other infra changes in the same Terraform states in the meantime.&lt;/p&gt;
&lt;p&gt;We have to deploy module A first because it contains fundamental resources (such as networking: VNets, subnets, endpoints) that all subsequent modules depend on.
So the deployment order is: &lt;code&gt;A -&amp;gt; B&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s consider two scenarios.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Migrate a resource from A to B&lt;/li&gt;
&lt;li&gt;Migrate a resource from B to A&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;rsquo;s say you migrate a resource &lt;em&gt;from module A to B&lt;/em&gt;.
First you remove the resource block from the infrastructure as code.
If you forget to correctly remove the resource from the Terraform state with a &lt;code&gt;removed&lt;/code&gt; block in A, applying the Terraform plan will delete the resource and then re-create it at B.
In this case you have &lt;em&gt;de facto&lt;/em&gt; migrated, but if your resources contains data you have just messed up.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s say you migrate a resource &lt;em&gt;from module B to A&lt;/em&gt;.
If you forget to make and inspect a Terraform plan for &lt;em&gt;all involved modules&lt;/em&gt; and find out too late that your &lt;code&gt;removed&lt;/code&gt; blocks from the Terraform state of B contain mistakes, you wil happily delete the resources that you have just imported in the state of A.
This scenario is even more nasty, because at the end of the pipeline you have not only possibly thrown away data or identities tied to that specific resources, but also end up without that resource altogether.
This requires manual intervention to fix.&lt;/p&gt;
&lt;h3 id="do-not-apply-iac-from-different-branches"&gt;Do not apply IaC from different branches &lt;a href="#do-not-apply-iac-from-different-branches"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When you are migrating resources from a refactoring branch, it&amp;rsquo;s very important to communicate with your teammates when the actual migration starts in order to ensure nobody manipulates the Terraform states from another branch with different IaC.&lt;/p&gt;
&lt;p&gt;Some important safeguards should be in place:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;main&lt;/code&gt; branch is the single source of truth, do not deploy any infrastructure (except for your dev environment) from other branches. Enforce this in your deployment pipelines.&lt;/li&gt;
&lt;li&gt;In staging and production environments, &lt;em&gt;nobody&lt;/em&gt; should be able to manually deploy infrastructure using a &lt;code&gt;terraform apply&lt;/code&gt;, but nevertheless you may have forgotten to deactivate an older deployment pipeline that &lt;em&gt;can&lt;/em&gt; apply changes and may automatically trigger. If the previous safeguard is in place you should nevertheless only be able to deploy from the main branch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If these guardrails are in place, you can only mess up on the development environment.
For the sake of argument, let&amp;rsquo;s think through what happens if multiple people apply different IaC on the development environment.&lt;/p&gt;
&lt;figure style="width: 50%; margin-left: 20px; margin-bottom: 10px; float: right;"&gt;
&lt;img src="https://upload.wikimedia.org/wikipedia/en/d/db/Hank_Schrader_S5B.png" alt="Hank" /&gt;
&lt;figcaption&gt;Hank, looking worried&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is Hank.
Hank was living the good life and followed this migration guide.
In the &lt;code&gt;A -&amp;gt; B&lt;/code&gt; scenario he is doing a dry run of his migration by applying the infra changes on the development environment before merging his IaC to the main branch and committing to the whole migration.&lt;/p&gt;
&lt;p&gt;But now he found out that someone he thought he knew well is not who he seemed to be.
That persons is applying old IaC to the development environment and on top of that, is &lt;em&gt;breaking bad&lt;/em&gt; completely because they didn&amp;rsquo;t even check the Terraform plan before applying.
Now Hank is worried and wondering why his database disappeared on dev.&lt;/p&gt;
&lt;p&gt;What happened?&lt;/p&gt;
&lt;p&gt;Hank migrated a database from Terraform state B to state A on his development branch.
During his testing run on dev, he rolled out these changes.
Our bad guy rolls out the old IaC from another branch.
Terraform notices that there is a database in state A that is not in the IaC, so will happily generate a plan to throw away the database.&lt;/p&gt;
&lt;p&gt;An optional guardrail is to use &lt;code&gt;data&lt;/code&gt; blocks for resources that actually contain data, such as databases or critical storage accounts.
It depends really on whether you want to live dangerously or not:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For believe me: the secret for harvesting from existence the greatest fruitfulness and greatest enjoyment is —- to live dangerously. - Friedrich Nietzsche&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="disaster-recovery"&gt;Disaster recovery &lt;a href="#disaster-recovery"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Before doing a migration, make a backup of your terraform states.
You can use &lt;code&gt;terraform state pull&lt;/code&gt; for this.&lt;/p&gt;
&lt;p&gt;What do you do when things go wrong anyways?
At this point, you have to accept shit is FUBAR and that you have to make manual interventions without making further mistakes under stress.&lt;/p&gt;
&lt;p&gt;Honestly, this is context dependent, but it&amp;rsquo;s not as simple as just reverting the IaC changes and restoring the old Terraform state (you could use &lt;code&gt;terraform state push&lt;/code&gt;, which has some safeguards built-in).
If things went bad, your infrastructure has probably changed so your old Terraform states are outdated.
If it&amp;rsquo;s possible to recover deleted or changed infrastructure (e.g. if you have soft deletes and recovery procedures) to exactly match the old IaC, you could try to revert IaC changes and restore the old state to reach a point where you end up with a neutral Terraform plan.
The downside? Not only have you possibly thrown away data, you are back to square one with the migration.&lt;/p&gt;
&lt;p&gt;Instead, I&amp;rsquo;d personally adopt a &amp;ldquo;fix forward&amp;rdquo; strategy: accept you messed up, make sure all resources are rolled out like they were supposed to, and make ad-hoc changes to the Terraform states using imports and removals.&lt;/p&gt;
&lt;h2 id="some-terraform-gotchas-as-of-march-2024"&gt;Some Terraform gotchas (as of March 2024) &lt;a href="#some-terraform-gotchas-as-of-march-2024"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To conclude, some gotchas I encountered during a migration I performed recently.&lt;/p&gt;
&lt;p&gt;Import blocks still do not support the &lt;code&gt;count&lt;/code&gt; keyword, but since recently they do support &lt;code&gt;for_each&lt;/code&gt;.
Resources created with a count parameter are &lt;em&gt;indexed&lt;/em&gt; with an integer value, like &lt;code&gt;azurerm_storage_account.storage_account[0]&lt;/code&gt;.
This index &lt;em&gt;must&lt;/em&gt; be numerical and cannot be a string.
If you have a high count (let&amp;rsquo;s say 100) or the amount of resources is non-static because it depends on a variable that&amp;rsquo;s different for each deployment environment, you do not want to manually type 100 import blocks.
What you want to do instead is create a list or mapping that you can pass to &lt;code&gt;for_each&lt;/code&gt; such that you can create a numerical range.
You can pass a list to &lt;code&gt;for_each&lt;/code&gt;, but this will implicitly create a map with identical keys and values.
Be careful with Terraform map objects, because the &lt;em&gt;key is always a string&lt;/em&gt; even if you try to cast it to a number.
You will end up with &lt;code&gt;azurerm_storage_account.storage_account[&amp;quot;0&amp;quot;]&lt;/code&gt; and this will not work for obvious reasons.
Instead, just use the &lt;code&gt;range&lt;/code&gt; function and use &lt;code&gt;each.value&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Also don&amp;rsquo;t forget to update to the latest Terraform version, because the &lt;code&gt;for_each&lt;/code&gt; argument to &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;removed&lt;/code&gt; blocks are only available since 1.7.&lt;/p&gt;
&lt;p&gt;Related notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="./zettelkasten/202403130820-terraform_migration_guide/"&gt;Terraform migration guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Self portraits using stable diffusion</title><link>https://www.edwinwenink.xyz/posts/72-stable_diffusion_portraits/</link><pubDate>Sat, 09 Sep 2023 16:21:46 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/72-stable_diffusion_portraits/</guid><description>&lt;p&gt;I was in need of a head shot, but I don&amp;rsquo;t like taking pictures.
Being a programmer I figured, why not let AI magically turn a messy selfie into a proper professional head shot?
As it turns out, there&amp;rsquo;s quite a market for AI tools that generate professional portraits that are suitable for LinkedIn and such, but they were requesting fees I wasn&amp;rsquo;t willing to pay.
I hoped to do some experiments on a Midjourney trial license instead, but they discontinued those.
&lt;em&gt;No problemo&lt;/em&gt;, this is how this post was conceived: let&amp;rsquo;s see how far we get with a pretrained &lt;em&gt;stable diffusion&lt;/em&gt; model without further fine-tuning on my face.&lt;/p&gt;
&lt;p&gt;I used
&lt;a href="https://huggingface.co/CompVis/stable-diffusion" target="_blank"&gt;CompVis/stable-diffusion-v1-4&lt;/a&gt;, which was initially trained on 256x256 images from
&lt;a href="https://laion.ai/blog/laion-aesthetics/" target="_blank"&gt;laion-aesthetic&lt;/a&gt; data sets and further fine-tuned on 512x512 images.
This is a text-to-image model trained to generate images based on a text prompt describing the scene, but it&amp;rsquo;s also possible to initialize the inference process with a reference picture.
It&amp;rsquo;s possible to tweak the relative strength of the reference image and the &amp;ldquo;hallucination&amp;rdquo; of the model based on the prompt.
I played around with two selfies that I cropped to a 1:1 ratio and then resized to 512x512.
The first reference picture was a selfie with my &lt;em&gt;huge&lt;/em&gt; Monstera in the background.
The &amp;ldquo;head and shoulders&amp;rdquo; setup is also quite common for portraits so I expected this layout to be well represented in the training data.&lt;/p&gt;
&lt;figure style="width: 50%; margin-left: 20px; margin-bottom: 10px; float: right;"&gt;
&lt;img src="./images/diffusion/not_quite_edwin.jpg" alt="Dreams of Edwin." /&gt;
&lt;figcaption&gt;Dreams of Edwin.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;It&amp;rsquo;s not hard to generate stunning pictures if you let the model hallucinate.
The top two pictures are very loosely inspired by the reference picture (the green from the Monstera subtly returns in the background; heavy mustache) but don&amp;rsquo;t look like me at all.
The next two pictures attribute more importance to the reference picture and stay close to the original layout of the scene and main characteristics of my face (mainly my hair and beard style), but other than that didn&amp;rsquo;t look like me at all.
My main observation at this point is that &amp;hellip;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;trying to stay close to a reference portrait without fine-tuning on the subject is a one-way ticket to the &lt;em&gt;uncanny valley&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was initially sceptical, but by now I&amp;rsquo;m convinced that using generative AI in this manner to create a specific picture you have in mind truly is a creative process.
It is similar to how a professional photographer sets up the environment for the right snapshot: the art is not in the pressing of the button, but on using the tools available to materialize something that only existed in your mind&amp;rsquo;s eye so far.
Similarly, prompt engineering is an art in the sense that over time you start to get a feeling for which prompts are successful for manipulating this stochastic &amp;ldquo;obscene hallucination machine&amp;rdquo; in a desired direction.&lt;/p&gt;
&lt;figure style="width: 50%; margin-left: 20px; margin-bottom: 10px; float: right;"&gt;
&lt;img src="./images/diffusion/edwin_alphone_mucha.jpg" alt="Edwin interpreted in jugendstil à la Alphone Mucha." /&gt;
&lt;figcaption&gt;Edwin interpreted in jugendstil à la Alphone Mucha.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure style="width: 50%; margin-left: 20px; margin-bottom: 10px; float: right;"&gt;
&lt;img src="./images/diffusion/edwin_and_vincent.jpg" alt="Edwin and Vincent." /&gt;
&lt;figcaption&gt;Edwin and Vincent.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Only after including the names of painters and artists I started seeing results that I liked.
For example, I added jugendstil artist Alphone Mucha to the prompt.
These results are obviously not &amp;ldquo;realistic,&amp;rdquo; but in for example the top left picture I started recognizing a bit of myself in the glance of the eyes.
The style worked really well with the Monstera in the background.&lt;/p&gt;
&lt;p&gt;I tend to be compared to Vincent van Gogh and you can probably see why in the final results.
These are based on a different picture that I sent to my better half when I was, quite frankly, done with my day.
When I threw Vincent van Gogh and Edvard Munch in the mix, I finally managed to produce a stylized version of a picture that actually looked like me.
Lo and behold, the top left picture is a stylized and &amp;ldquo;painted&amp;rdquo; version of what I actually look like.
Everyone that knows me will immediately recognize me in this picture.&lt;/p&gt;
&lt;p&gt;To achieve the goal of getting a professional looking head shot, the next step is to finetune a stable diffusion model on pictures of my own face.
This should make it possible to let the model do its magic on the background and the styling, but stay closer to my actual face.&lt;/p&gt;</description></item><item><title>My Vinyl Record Collection</title><link>https://www.edwinwenink.xyz/posts/70-my_vinyl_record_collection/</link><pubDate>Tue, 29 Aug 2023 12:03:02 +0200</pubDate><guid>https://www.edwinwenink.xyz/posts/70-my_vinyl_record_collection/</guid><description>&lt;figure style="width: 50%; margin-left: 20px; margin-bottom: 10px; float: right;"&gt;
&lt;img src="https://thevinylwall.com/cdn/shop/files/head_alternate_edit.jpg?v=1613523692" alt="Vinyl wall mount" /&gt;
&lt;figcaption&gt;&lt;a href="https://thevinylwall.com/"&gt;Analog example&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I added a digital
&lt;a href="./records/"&gt;vinyl record display&lt;/a&gt; to this website, have a look!&lt;/p&gt;
&lt;p&gt;The idea was to digitally reproduce one of those fancy wall mounts for displaying records in your room.
You can click on each record to see some basic information.&lt;/p&gt;
&lt;p&gt;All vinyl records I physically own are registered on
&lt;a href="https://www.discogs.com/" target="_blank"&gt;Discogs&lt;/a&gt;.
I used the Discogs API to automatically retrieve my collection, parse the information I want, and store all results in Markdown files that can be consumed on this website.&lt;/p&gt;
&lt;p&gt;If you like what you see, I
&lt;a href="https://github.com/EdwinWenink/discogs_vinyl_record_display" target="_blank"&gt;open sourced my code&lt;/a&gt; to reproduce these results so you can make your own digital wall too.
The project&amp;rsquo;s README explains how to run the project using your own Discogs collection.
The only thing you need is a Discogs user name, Python, and optionally an authentication token to get links to cover images.
Also note that Discogs throttles requests per minute, so check out my tip on deployment too.&lt;/p&gt;
&lt;p&gt;If you like this project, give it a star on Github and share a link to your digital record collection.
I&amp;rsquo;d love to see it!&lt;/p&gt;</description></item><item><title>Version control on notebooks using pre-commit and Jupytext</title><link>https://www.edwinwenink.xyz/posts/71-version_control_on_notebooks_with_precommit_and_jupytext/</link><pubDate>Sun, 29 Jan 2023 16:21:46 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/71-version_control_on_notebooks_with_precommit_and_jupytext/</guid><description>&lt;p&gt;Notebooks have a place and a time.
They are suitable for sharing the insights of an exploratory data analysis, but not so convenient for collaborating with multiple people whilst having the notebook code under version control.
Generally speaking notebooks do not promote good coding habits, for example because people tend to duplicate code by copying cells.
People typically also don&amp;rsquo;t use supportive tooling such as code linters and type checkers.
But one of the most nasty problems with notebooks is that version control is hard, for several reasons.
Running a diff on notebooks simply sucks.
The piece of text we care about when reviewing - the code - is hidden in a JSON-style data structure that is interleaved with base-64 encoded blobs for images or binary cell outputs.
Particularly these blobs will clutter up your diffs and make it very hard to review code.
So how can we use notebooks with standard versioning tools such as Git?
This post explains an automated workflow for good version control on Jupyter and Jupyter Lab notebooks using Git, the pre-commit framework, and Jupytext.
The final pre-commit configuration can be found &lt;a href='#putting-it-all-together'&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="which-type-of-notebooks-need-a-version-control-workflow"&gt;Which type of notebooks need a version control workflow? &lt;a href="#which-type-of-notebooks-need-a-version-control-workflow"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;But let&amp;rsquo;s first take a step back.
Whether we actually need version control depends on what the purpose of the notebook is.
We can roughly distinguish two types of notebooks.&lt;/p&gt;
&lt;p&gt;Firstly, we have exploratory notebooks that can be used for experimentation, early development, and exploration.
This type of notebook should be treated as a historical and dated (possibly outdated!) record of some analysis that provided insights at some moment in a project.
In this sense the notebook code does not have to be up to date with the latest changes.
For this type of &amp;ldquo;lab&amp;rdquo; notebook, most difficulties concerning version control can be avoided by following these recommendations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exploratory notebooks should have a &lt;em&gt;single&lt;/em&gt; author who is the owner of the analysis&lt;/li&gt;
&lt;li&gt;A minimal version control approach is possible: others do not push changes in that notebook, unless explicitly coordinated with the owner of the notebook.
&lt;ul&gt;
&lt;li&gt;This should avoid having to review unreadable diffs&lt;/li&gt;
&lt;li&gt;And also avoid merge conflicts altogether&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Mention the &lt;em&gt;date&lt;/em&gt; and the &lt;em&gt;author name&lt;/em&gt; of the analysis in the file name to make the historical character and owner explicit&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Secondly, in some workflows one may want to collaborate with multiple people on the same notebook or even have notebooks in production (looking at you, Databricks).
In these cases, notebooks store the most up-to-date and polished up output of some analysis that can be presented to stakeholders.
But the most notable difference is that this type of notebook is the responsibility of the &lt;em&gt;whole&lt;/em&gt; data science team.
As a result, the notebook workflow should support more advanced version control, such as the code review process via pull requests and handling merge conflicts.
The rest of this post explains such a workflow.&lt;/p&gt;
&lt;h2 id="general-recommendations"&gt;General recommendations &lt;a href="#general-recommendations"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To start, I make these general recommendations for all notebooks that are committed to a Git repository:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clear all cell outputs before committing a notebook to the repository
&lt;ul&gt;
&lt;li&gt;This avoids cluttering Git diffs with anything other than code, although this will not prevent changes in metadata to be pushed.&lt;/li&gt;
&lt;li&gt;If notebook output has to be put under version control, convert the notebook to html (using &amp;ldquo;save as&amp;rdquo; or &lt;code&gt;nbconvert&lt;/code&gt;) and commit the html instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Store notebooks apart from the main source code of a project. I personally use a dedicated &lt;code&gt;notebooks&lt;/code&gt; folder.
&lt;ul&gt;
&lt;li&gt;This also allows you to more easily trigger a separate workflow for notebooks (see below).&lt;/li&gt;
&lt;li&gt;This keeps the main source code of a project uncluttered with dated notebooks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="motivation-of-the-chose-workflow"&gt;Motivation of the chose workflow &lt;a href="#motivation-of-the-chose-workflow"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The core idea of this workflow is to &lt;em&gt;convert&lt;/em&gt; notebooks to regular scripts and use these to create meaningful diffs for reviewing.
This basic workflow &lt;em&gt;could&lt;/em&gt; work something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When you need to diff a notebook, export to Python using &lt;code&gt;jupyter nbconvert&lt;/code&gt; and diff the Python file instead.&lt;/li&gt;
&lt;li&gt;When you need output, use &lt;code&gt;nbconvert&lt;/code&gt; to execute the notebook and convert the output to html&lt;/li&gt;
&lt;li&gt;Before committing a notebook, clear the outputs to minimize unreadable blobs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To convert to HTML: &lt;code&gt;jupyter nbconvert /notebooks/example.ipnb --output-dir=&amp;quot;/results&amp;quot; --output=&amp;quot;example.html&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To convert to Python: &lt;code&gt;jupyter nbconvert /notebooks/example.ipnb --to=&amp;quot;python&amp;quot; --output-dir=&amp;quot;/results&amp;quot; --output=&amp;quot;example&amp;quot;&lt;/code&gt; or the newer syntax &lt;code&gt;jupyter nbconvert --to script notebook.ipynb&lt;/code&gt;.
Notice the absence of the &lt;code&gt;.py&lt;/code&gt; extension if you specify a &lt;code&gt;to&lt;/code&gt; argument.
Multiple notebooks can be converted using a wildcard.&lt;/p&gt;
&lt;p&gt;If you want to have executed notebooks in your production environment, you can 1) commit the Python version of the notebook and 2) execute the notebook with a call to &lt;code&gt;nbconvert&lt;/code&gt; in a pipeline, such as Github Actions (
&lt;a href="https://github.com/dliden-bitdotio/bls-notebook" target="_blank"&gt;example GitHub Actions workflow&lt;/a&gt;).
This allows you to execute a notebook without opening the notebook in your browser.
You execute a notebook with cleared outputs as such:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;jupyter nbconvert --to notebook --execute notebook_cleared.ipynb --output notebook_executed&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;However, a downside of &lt;code&gt;nbconvert&lt;/code&gt; is that the conversion is uni-directional from notebook to script, so one cannot reconstruct the notebook after making changes in the generated script.
In other words, the corresponding script would be used strictly for reviewing purposes, but the notebook would stay the single source of truth.
The &lt;code&gt;jupytext&lt;/code&gt; tool solves this problem by doing a bi-directional synchronization between a notebook and its corresponding script.&lt;/p&gt;
&lt;p&gt;We should also note that this workflow requires several &lt;em&gt;manual&lt;/em&gt; steps that can be easily forgotten and messed up.
It is possible to write post-save Jupyter hooks to automate these steps, but a limitation is that such a configuration would be user-specific.
It is also possible to use Git hooks, but these are project-specific and would require all team members to copy in the correct scripts in a project&amp;rsquo;s &lt;code&gt;.git&lt;/code&gt; folder.
Instead, we&amp;rsquo;d like to describe a workflow that can be installed in a new project in a uniform manner, such that each team member is guaranteed to use the same workflow.
We&amp;rsquo;ll use the multi-language &lt;code&gt;pre-commit&lt;/code&gt; framework to install a &lt;code&gt;jupytext&lt;/code&gt; synchronization hook.&lt;/p&gt;
&lt;h2 id="what-is-jupytext"&gt;What is Jupytext? &lt;a href="#what-is-jupytext"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Jupytext is a tool for two-way synchronization between a Jupyter notebook and a paired script in
&lt;a href="https://jupytext.readthedocs.io/en/latest/languages.html" target="_blank"&gt;many languages&lt;/a&gt;.
In this post we only consider the &lt;code&gt;.ipynb&lt;/code&gt; and &lt;code&gt;.py&lt;/code&gt; extensions.
It can be used as a standalone command line tool or as a plugin for Jupyter Notebooks or Jupyter Lab.
To save information on notebook cells Jupytext either uses a minimalistic &amp;ldquo;light&amp;rdquo; encoding or a &amp;ldquo;percent&amp;rdquo; encoding (the default).
We will use the percent encoding.
The following subsections are applicable only if you want to be able to use Jupytext as a standalone tool.
If not, skip ahead to &lt;a href="combining-jupytext-with-pre-commit"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="installation-and-pairing"&gt;Installation and pairing &lt;a href="#installation-and-pairing"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Install as standalone command using pip
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pip install jupytext --upgrade&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Handy! Can be included in requirements.txt&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Install as Jupyter notebook extension
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jupyter nbextension install --py jupytext [--user]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Follow by: &lt;code&gt;jupyter nbextension enable --py jupytext [--user]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Notebook: File -&amp;gt; Jupytext -&amp;gt; Pair Notebook with light (fewer markers) or percent (explicit cell delimiters) script&lt;/li&gt;
&lt;li&gt;If you now save the notebook, a corresponding .py version will be updated automatically!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Install as Jupyter lab extension
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;jupyter labextension install jupyterlab-jupytext&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Lab: View -&amp;gt; Activate Command Palette (Ctrl+Shift+C) -&amp;gt; Pair Notebook with &amp;hellip;&lt;/li&gt;
&lt;li&gt;If you now save the notebook, a corresponding .py version will be updated automatically!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="basic-usage"&gt;Basic usage &lt;a href="#basic-usage"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;When you save a paired notebook in Jupyter, both the &lt;code&gt;.ipynb&lt;/code&gt; file and the script version are updated on disk.&lt;/p&gt;
&lt;p&gt;On the command line, you can update paired notebooks using &lt;code&gt;jupytext --sync notebook.ipynb&lt;/code&gt; or &lt;code&gt;jupytext --sync notebook.py&lt;/code&gt;.
If you run this on a new script, Jupytext will encode the script and define cells using some basic rules (e.g. delimited by newlines), then convert it to a paired notebook.&lt;/p&gt;
&lt;p&gt;When a paired notebook is opened or reloaded in Jupyter, the input cells are loaded from the text file, and combined with the output cells from the &lt;code&gt;.ipynb&lt;/code&gt; file.
This also means that when you edit the &lt;code&gt;.py&lt;/code&gt; file, you can update the notebook simply by &lt;em&gt;reloading&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;You can specify a project specific configuration in &lt;code&gt;jupytext.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;formats = &amp;#34;ipnb,py:percent&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To convert a notebook to a notebook &lt;em&gt;without outputs&lt;/em&gt;, use &lt;code&gt;jupytext --to notebook notebook.py&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="combining-jupytext-with-pre-commit"&gt;Combining Jupytext with pre-commit &lt;a href="#combining-jupytext-with-pre-commit"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Okay, so Jupytext handles the two-way synchronization between scripts and outputs, which is an improvement compared to Jupyter&amp;rsquo;s native &lt;code&gt;nbconvert&lt;/code&gt; command.
The basic idea is still that when you want notebook code reviewed, collaborators can instead read and comment on the paired script.
It is now also possible to incorporate the feedback directly in the script if we wish to do so, because the notebook will be updated accordingly.&lt;/p&gt;
&lt;p&gt;Our broader goal was to completely remove any need for manual steps.
We will automate the synchronization step using a pre-commit hook, which means that we check the synchronization status before allowing work to be committed.
This is a safeguard to avoid to avoid that out of sync notebooks and scripts are committed.&lt;/p&gt;
&lt;p&gt;Git hooks are very handy, but they go in &lt;code&gt;.git/hooks&lt;/code&gt; and are therefore not easily shared across projects.
The &lt;code&gt;pre-commit&lt;/code&gt; package is designed as a multi-language package manager for pre-commit hooks and can be installed using pip: &lt;code&gt;pip install pre-commit&lt;/code&gt;.
It allows you to specify pre-commit hooks in a declarative style and also manages dependencies, so if we declare a hook that uses Jupytext we do not even have to manually install Jupytext.
We declare the hooks in a configuration file.
We also automate the &amp;ldquo;clear output cells&amp;rdquo; step mentioned above using &lt;code&gt;nbstripout&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;My &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; for syncing notebooks and their script version looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;repos&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/kynan/nbstripout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.6.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;nbstripout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/mwouts/jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1.14.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Runs jupytext on all notebooks and paired files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;language&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;entry&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext --pre-commit-mode --set-formats &amp;#34;ipynb,py:percent&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;require_serial&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;types_or&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;jupyter, python]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;files&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;^notebooks/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Only apply this under notebooks/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;After setting up a config, you have to &lt;em&gt;install&lt;/em&gt; the hook as a git hook: &lt;code&gt;pre-commit install&lt;/code&gt;.
This clones Jupytext and sets up the git hook using it.
Now the defined commit wil run when you &lt;code&gt;git commit&lt;/code&gt; and you have defined the hook in a language agnostic way!
In this configuration, you manually specify the &lt;code&gt;rev&lt;/code&gt; which is the tag of the specifiek &lt;code&gt;repo&lt;/code&gt; to clone from.
You can test if the hook runs by running it on a specific file or on all files with &lt;code&gt;pre-commit run --all-files&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="explanation-and-possible-issues"&gt;Explanation and possible issues &lt;a href="#explanation-and-possible-issues"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;There are some important details when using Jupytext as a pre-commit hook.
The first &lt;em&gt;gotcha&lt;/em&gt; is that when checking whether paired notebooks and scripts are in sync, it actually runs Jupytext and synchronizes paired scripts and notebooks.
If the paired notebooks and scripts were out of sync, running the hook will thus &lt;em&gt;introduce unstaged changes&lt;/em&gt;!
These unstaged changes also need to be committed in Git before the hook passes and it is recognized that the files are in sync.&lt;/p&gt;
&lt;p&gt;The second gotcha is that the &lt;code&gt;--pre-commit-mode&lt;/code&gt; flag is important to avoid a subtle but very nasty loop.
The standard behavior of &lt;code&gt;jupytext --sync&lt;/code&gt; is to see which two of the paired files (notebook or script) was most recently edited and has to be taken as the ground truth for the two way synchronization.
This is problematic because this causes a loop when used in the pre-commit framework.
For example, let&amp;rsquo;s say that we have a paired notebook and script and that we edit the script.
When we commit the changes in the script, the pre-commit hook will first run Jupytext.
In this case the script is the &amp;ldquo;source of truth&amp;rdquo; for the synchronization such that the notebook needs to be updated.
The Jupytext pre-commit hook check will fail because we now have unstaged changed in the updated notebook that we need to commit to Git.
When we commit the changes in the updated notebook, however, the notebook becomes the most recently edited file, such that Jupytext complains that it is now unclear whether the notebook or the script is the source of truth.&lt;/p&gt;
&lt;p&gt;The good news is that Jupytext is smart enough to raise an error and requires you to manually specify which changes to keep.
The bad news is that in this specific case, this manual action does not prevent the loop: we&amp;rsquo;re in a Catch 22 of sorts!
The &lt;code&gt;--pre-commit-mode&lt;/code&gt; fixes this nasty issue by making sure that Jupytext does not always consider the most recently edited file as the ground truth.&lt;/p&gt;
&lt;p&gt;Within the pre-commit framework you almost certainly also want to specify other hooks.
For example, I want to make sure my code is &lt;code&gt;PEP8&lt;/code&gt; compliant by running &lt;code&gt;flake8&lt;/code&gt; or some other linter on the changes that are to be committed.
The pre-commit framework itself also offers hooks for fixing common code issues such as trailing whitespaces or left-over merge conflict markers.
But this is where I&amp;rsquo;ve encountered another nasty issue that prevented the Jupytext hook from correctly syncing.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s take a hook that removes trailing white spaces as an example.
This hook works as intended on Python scripts, but the hook does not actually remove trailing white spaces &lt;em&gt;in the code&lt;/em&gt; because source code of notebook cells are encapsulated in a JSON field as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;cell_type&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;code&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;execution_count&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;361b20cc&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;metadata&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;gather&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;logged&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1671533986727&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;jupyter&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;outputs_hidden&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;source_hidden&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;nteract&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;transient&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;deleting&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;outputs&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;source&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;#### pre-processing\n&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;from src.preprocessing import preprocess_and_split\n&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;\n&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;df_indirect, df_direct, df_total = preprocess_and_split(df)&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This means that if you commit a notebook with trailing white spaces in the cells, the following happens and will prevent the paired notebook and script from ever syncing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Jupytext hook synchronizes the notebook with a paired Python script.&lt;/li&gt;
&lt;li&gt;Trailing whitespaces are removed from the Python script and the &lt;em&gt;plain text&lt;/em&gt; representation of the notebook (i.e. it removes trailing white space after the closing brackets)&lt;/li&gt;
&lt;li&gt;When translating the notebook to code or vice versa, one version still includes trailing white spaces in the code cells and the other not.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is something to be wary of and needs to be solved on a case to case basis.
I have solved this specific issue by adding all notebooks in the &lt;code&gt;notebooks&lt;/code&gt; folder as per my recommendation and then not running the trailing whitespace hook in the &lt;code&gt;notebooks&lt;/code&gt; folder.&lt;/p&gt;
&lt;h2 id="putting-it-all-together"&gt;Putting it all together &lt;a href="#putting-it-all-together"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The following is a &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; that synchronizes all notebooks with Python scripts under &lt;code&gt;notebooks&lt;/code&gt; and plays nice with other pre-commit hooks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Install the pre-commit hooks below with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# &amp;#39;pre-commit install&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# Run the hooks on all files with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# &amp;#39;pre-commit run --all&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;repos&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/pre-commit/pre-commit-hooks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v4.4.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# See: https://pre-commit.com/hooks.html&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;trailing-whitespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;types&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;python]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;exclude&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;^notebooks/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Avoids Jupytext sync problems&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;end-of-file-fixer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;check-merge-conflict&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;check-added-large-files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;args&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#39;--maxkb=5000&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;end-of-file-fixer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;mixed-line-ending&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Run flake8. Give warnings, but do not reject commits&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/PyCQA/flake8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;6.0.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;flake8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;args&lt;/span&gt;: [--&lt;span style="color:#ae81ff"&gt;exit-zero] &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Do not reject commit because of linting&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;verbose&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Enforce that notebooks outputs are cleared&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/kynan/nbstripout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.6.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;nbstripout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# Synchronize notebooks and paired script versions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;- &lt;span style="color:#f92672"&gt;repo&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;https://github.com/mwouts/jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;rev&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1.14.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;hooks&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;id&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;description&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Runs jupytext on all notebooks and paired files&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;language&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;python&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;entry&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;jupytext --pre-commit-mode --set-formats &amp;#34;ipynb,py:percent&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;require_serial&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;types_or&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;jupyter, python]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;files&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;^notebooks/ &lt;/span&gt; &lt;span style="color:#75715e"&gt;# Only apply this under notebooks/&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The final workflow for version control on notebooks is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Install Git pre-commit hooks in your project based on the declarative config using &lt;code&gt;pre-commit install&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Save notebooks under the &lt;code&gt;notebooks&lt;/code&gt; folder&lt;/li&gt;
&lt;li&gt;Work on your Jupyter notebooks as usual&lt;/li&gt;
&lt;li&gt;Commit your work. This triggers the pre-commit hook&lt;/li&gt;
&lt;li&gt;If the pre-commit hook fails and introduces new changes, commit those changes too for the hook checks to pass&lt;/li&gt;
&lt;li&gt;Now all checks should pass and you are free to commit and push!&lt;/li&gt;
&lt;li&gt;When you make a PR, collaborators can now provide comments on the paired script&lt;/li&gt;
&lt;li&gt;Feedback can be incorporated either in the notebook or the script since they are synchronized anyways&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here we have assumed you have created and specified &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; and &lt;code&gt;jupytext.toml&lt;/code&gt; in your project root.&lt;/p&gt;
&lt;h2 id="further-reading"&gt;Further reading &lt;a href="#further-reading"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://jupytext.readthedocs.io/en/latest/index.html" target="_blank"&gt;Jupytext docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://jupytext.readthedocs.io/en/latest/examples.html" target="_blank"&gt;Jupytext docs on collaborating on notebooks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;There are some notebook aware tools for diffing and merging
&lt;ul&gt;
&lt;li&gt;E.g. &lt;code&gt;nbdiff&lt;/code&gt; and &lt;code&gt;nbmerge&lt;/code&gt; commands from
&lt;a href="https://nbdime.readthedocs.io/en/latest/" target="_blank"&gt;nbdime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ReviewNB GitHub app&lt;/li&gt;
&lt;li&gt;Neptune&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.svds.com/jupyter-notebook-best-practices-for-data-science/" target="_blank"&gt;https://www.svds.com/jupyter-notebook-best-practices-for-data-science/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://timstaley.co.uk/posts/making-git-and-jupyter-notebooks-play-nice/" target="_blank"&gt;http://timstaley.co.uk/posts/making-git-and-jupyter-notebooks-play-nice/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://innerjoin.bit.io/automate-jupyter-notebooks-on-github-9d988ecf96a6" target="_blank"&gt;https://innerjoin.bit.io/automate-jupyter-notebooks-on-github-9d988ecf96a6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://nextjournal.com/schmudde/how-to-version-control-jupyter" target="_blank"&gt;https://nextjournal.com/schmudde/how-to-version-control-jupyter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Overgeven of Sneuvelen: De Atjeh-oorlog</title><link>https://www.edwinwenink.xyz/posts/69-de_atjeh_oorlog/</link><pubDate>Sun, 13 Nov 2022 12:03:02 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/69-de_atjeh_oorlog/</guid><description>&lt;blockquote&gt;
&lt;p&gt;In 1873 valt Nederland de zelfstandige staat Atjeh binnen, gelegen op de noordpunt van Sumatra.
De Nederlanders verwachten Atjeh snel te kunnen onderwerpen.
Dat blijkt een illusie te zijn.
Ze belanden in een oorlog die de langste en bloedigste uit de Nederlandse koloniale geschiedenis zal worden: de Atjeh-oorlog.&lt;br&gt;
&lt;br&gt;
&lt;em&gt;Overgeven of sneuvelen&lt;/em&gt; is de enige roman in de Nederlandse literatuur die de volledige Atjeh-oorlog beschrijft.
Ik vraag de auteur, Bert Wenink, wat hem heeft gedreven vijf jaar aan dit boek te werken.
Bestel het boek via
&lt;a href="https://bertwenink.nl/bestellen" target="_blank"&gt;zijn website&lt;/a&gt; of via
&lt;a href="https://www.boekenbestellen.nl/boek/overgeven-of-sneuvelen/9789083254203" target="_blank"&gt;boekenbestellen.nl&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;EW: Waarom heb je een boek over de Atjeh-oorlog geschreven?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Als ik het over de Atjeh-oorlog heb zie je mensen denken: wat voor een fascinatie heeft een witte man zonder Indische connecties, zonder voorvaderen die in de Oost hebben gevochten, met de Atjeh-oorlog? Mijn antwoord is dan: verwondering, sentiment en inbeelding.
Als kind logeerde ik regelmatig bij mijn oma.
Ze woonde in Arnhem, in een zijstraat van de Velperweg, dezelfde weg waaraan ook Bronbeek ligt, het museum van de Nederlandse geschiedenis in Indië en rusthuis voor veteranen die in het Koninklijk Nederlands Indisch Leger hadden gediend.
Daar zaten ze met goed weer in de tuin: de oude mannetjes in uniform met lange grijze baarden, de ex-KNILers.
Met die bijzondere, toen al anachronistische verschijning zijn waarschijnlijk de eerste zaadjes van fascinatie geplant.
Later bezocht ik met mijn vader het museum.
Ik kan me er weinig van herinneren, maar het moet indruk gemaakt hebben.
Het was nog de tijd van het tentoonstellen van exotische trofeeën, van krissen, speren en klewangs aan de muren en buitgemaakte kanonnen in de gangen.&lt;/p&gt;
&lt;!--![Kaart van Atjeh](/images/69/atjeh.png)--&gt;
&lt;figure&gt;
&lt;img align="right" alt="Kaart van Atjeh" style="width:50%; margin-left: 20px" src="./images/69/atjeh.png" /&gt;
&lt;/figure&gt;
&lt;p&gt;Nog weer later, veel later, ben ik drijvend op die sentimenten zelf terug gegaan, en toen begon de verwondering.
Daar in Bronbeek hing in de gang aan de muur een fragment uit de Atjeh-oorlog, geschreven door een zekere Schoemaker, een echte nationalist is me later gebleken toen ik meer van hem las.
Iemand die kritiekloos de heldendaden van &amp;lsquo;onze&amp;rsquo; jongens ophemelde, maar wel gezegend met een uitstekende pen waarmee hij taferelen levensecht tevoorschijn toverde.
Ik wilde steeds meer weten van die exotische, verbijsterende wereld die de Atjeh-oorlog is.
Nieuwsgierigheid doet de rest.
Waarom wagen jonge mannen zich in de hel van Indië? Wat deed één van Nederlands grootste geleerden uit de 19e eeuw op het slagveld van Atjeh, in een tijd dat de meeste geleerden gewoon achter hun bureau bleven zitten? En waarom vochten inheemse soldaten, in dienst van het KNIL, zo fanatiek voor de Nederlanders, waarom stormden die het eerst tegen de wand van een benteng op met het risico ook als eerste te sneuvelen? Al die vragen, waarbij de antwoorden je van de ene in de andere verbazing doen belanden, waren de drijfveren om steeds meer over de oorlog te lezen en er een roman over te schrijven.
Het schrijven was een soort ouderwetse negentiende-eeuwse ontdekkingstocht.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EW: Is het boek historisch verantwoord?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Overgeven of sneuvelen geeft een prima overzicht van de belangrijkste gebeurtenissen uit de Atjeh-oorlog.
Bij de historische personen heb ik mijn fantasie zo weinig mogelijk de ruimte gegeven.
Anders gezegd: ik laat Van Heutsz, Van Daalen, Sjech Saman di Tiro of Colijn geen verzonnen fratsen uithalen.
De echte romanpersonages hebben niet bestaan, maar hun belevenissen spelen zich af in een historische context en zouden echt zo gebeurd kunnen zijn.
Ik heb me zeer uitgebreid gedocumenteerd.
In mijn boek, en ook op mijn website, heb ik een lijst van ruim honderd geraadpleegde boeken en artikelen opgenomen.
Daarnaast heb ik een zeer uitvoerige correspondentie met een militair deskundige en Atjeh-kenner gevoerd.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EW: Je hebt dus veel historisch onderzoek gedaan. Waarom heb je dan toch de vorm van een roman gekozen?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Dat heeft met inbeelding te maken, en natuurlijk met mijn achtergrond; ik ben neerlandicus.
Ik zou zelf niet graag meemaken wat de KNIL-soldaten moesten doorstaan, maar meeleven in de vorm van literatuur is veilig genoeg om jezelf onder te dompelen in die ongelofelijke wereld.
Ik wilde de hele Atjeh-oorlog beschrijven, en dan met name door de ogen van personages en er ook nog een spannend verhaal met een plot van maken.
Een ambitieus plan.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EW: Kunnen we tegenwoordig nog iets leren van de Atjeh-oorlog?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Hoe vecht je tegen een vijand waarbij burgers en strijders nauwelijks te onderscheiden zijn? Waar kan een overhaast ten strijde trekken toe leiden? Vragen die nog steeds actueel zijn.
Het Atjese verzet was, net zoals in veel tegenwoordige oorlogen, islamitisch geïnspireerd.
De strategische lessen uit de Atjeh-oorlog gelden onverminderd voor hedendaagse oorlogen.
En wat mij betreft wierp het geweld uit de Atjeh-oorlog zijn schaduwen vooruit op de koloniale strijd na de Tweede Wereldoorlog.
Ik zou bijna zeggen: de Atjeh-oorlog is gruwelijk actueel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EW: De koloniale geschiedenis van Nederland in Indonesië staat momenteel volop in de aandacht. Recent onderzoek bevestigde bijvoorbeeld dat Nederland na WOII excessief geweld heeft gebruikt tijdens de Indonesische Onafhankelijkheidsoorlog. Jouw roman laat wellicht zien dat dat niet zo&amp;rsquo;n verrassing is gezien de gruwelijkheden van de Atjeh-oorlog.
Wat is jouw morele oordeel over de Atjeh-oorlog?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Verwondering en verbazing waren mijn drijfveren om steeds meer over de oorlog te lezen en er een roman over te schrijven.
Pas nadat ik &amp;rsquo;m af had kwamen andere, meer rationele vragen.
Over de lessen die we van de Atjeh-oorlog kunnen leren, over de beschaving die de Nederlanders wilden brengen, over de morele aspecten.
Mijn blik was – denk ik – niet oordelend, eerder met een zekere compassie voor de militairen, maar zeker niet vergoelijkend.
De lezer trekt zelf wel zijn conclusies.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EW: De roman neemt inderdaad geen stelling en biedt de lezer ruimte een eigen oordeel te vellen. Toch is het zo dat de roman vanuit het historische perspectief van Nederland is geschreven.
Is dat iets waar je mee hebt geworsteld tijdens het schrijven?
In hoeverre is het nodig en mogelijk het perspectief en de beweegredenen van de Atjeeërs te vertolken, of blijven zij toch de stemlozen van de koloniale geschiedenis?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;BW: Ik ben me er zeker van bewust dat de roman vanuit Nederlands perspectief is geschreven, al was het maar omdat ik was aangewezen op Nederlandse boeken, waarvan ook nog eens vaak voormalige officieren de auteurs waren.
Voor zover mij bekend is er trouwens nauwelijks documentatie van Atjese kant, en mocht die er zijn dan kan ik er niet mee uit de voeten, omdat ik de taal niet machtig ben.
De rol om de oorlog vanuit Atjees perspectief te belichten zou ik me trouwens sowieso niet graag toe-eigenen.
Ik pretendeer daarom zeker niet de Atjese kant van de oorlog goed te vertolken.
Toch hoop ik er wel indirect aan bij te dragen de Atjeeërs een stem te geven.
Te zorgen dat de oorlog niet vergeten wordt, lijkt me daarbij een eerste stap.&lt;/p&gt;</description></item><item><title>On circular imports in Python</title><link>https://www.edwinwenink.xyz/posts/68-python_circular_imports/</link><pubDate>Sat, 24 Sep 2022 14:00:45 +0200</pubDate><guid>https://www.edwinwenink.xyz/posts/68-python_circular_imports/</guid><description>&lt;p&gt;It has happened in the past that I&amp;rsquo;ve been sloppy with programming and took some shortcuts just to &amp;ldquo;get things done,&amp;rdquo; and that I encountered an error like the following: &lt;code&gt;AttributeError: module 'X' has no attribute 'call'&lt;/code&gt;.
This was quite baffling because module X &lt;em&gt;did&lt;/em&gt; have the attribute &lt;code&gt;call&lt;/code&gt;.
It turned out that I had accidentally did a very bad thing, namely to use a circular import that caused a function call to module X before that function was properly defined.
I knew I messed up, but in this post I dive deeper into &lt;em&gt;how&lt;/em&gt; I messed up.&lt;/p&gt;
&lt;h2 id="example"&gt;Example &lt;a href="#example"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s say we have a module X importing module Y:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# module X&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;Name:&amp;#34;&lt;/span&gt;, __name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;X start&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;call&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;You are calling a function of module X.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; __name__ &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;X main&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When we call this script, we call the &lt;code&gt;import Y&lt;/code&gt; statement first, which executes the code in module &lt;code&gt;Y&lt;/code&gt;.
Now let&amp;rsquo;s define module &lt;code&gt;Y&lt;/code&gt; with a circular import:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# module Y&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;Name:&amp;#34;&lt;/span&gt;, __name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;print(&lt;span style="color:#e6db74"&gt;&amp;#34;Y start&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;call()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;call&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;You are calling a function of module Y.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; __name__ &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; print(&lt;span style="color:#e6db74"&gt;&amp;#34;Y main&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now if we open an interactive terminal and import X, we run into trouble:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Y start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Traceback (most recent call last):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;stdin&amp;gt;&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;C:\Users\Edwin Wenink\Documents\programming-practice\Python\circular_import\X.py&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;C:\Users\Edwin Wenink\Documents\programming-practice\Python\circular_import\Y.py&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;call()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AttributeError&lt;/span&gt;: module &lt;span style="color:#e6db74"&gt;&amp;#39;X&amp;#39;&lt;/span&gt; has no attribute &lt;span style="color:#e6db74"&gt;&amp;#39;call&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is what happens:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We import module X&lt;/li&gt;
&lt;li&gt;The first line of module X import Y&lt;/li&gt;
&lt;li&gt;This executes the code in module Y&lt;/li&gt;
&lt;li&gt;Python sees X is already (partially being) imported, so the &lt;code&gt;import X&lt;/code&gt; statement in &lt;code&gt;Y&lt;/code&gt; will not trigger compilation of the content of X.&lt;/li&gt;
&lt;li&gt;This will print &amp;ldquo;Name: Y&amp;rdquo; and &amp;ldquo;Y start&amp;rdquo;&lt;/li&gt;
&lt;li&gt;Then it will run &lt;code&gt;X.call()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;But the &lt;code&gt;def call()&lt;/code&gt; statement in module &lt;code&gt;X&lt;/code&gt; has not been run yet, so we run into this error!&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="understanding-the-problem"&gt;Understanding the problem &lt;a href="#understanding-the-problem"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A possible solution could be to run &lt;code&gt;import Y&lt;/code&gt; only &lt;em&gt;after&lt;/em&gt; declaring the functions that are needed by module &lt;code&gt;Y&lt;/code&gt;.
If we do this, we get the following output without error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Y start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You are calling a function of module X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that importing Y with the above code gives no problems because X is defined before &lt;code&gt;X.call()&lt;/code&gt; runs, but &lt;em&gt;only&lt;/em&gt; because in this toy example X does currently not use the imported module Y:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Y start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You are calling a function of module X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In both cases, we see that the main function does not run because neither module is invoked as the main module.
The current situation is that importing X throws an error, but importing Y does not.
When &lt;em&gt;directly invoking&lt;/em&gt; the scripts as the main module, we see the opposite!
Calling X:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; python X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Y start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;You are calling a function of module X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: __main__
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;X main
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It took me a bit to understand why this does work now.
The difference is that now we do not start our execution with an &lt;code&gt;import X&lt;/code&gt; statement, so that in the first line of &lt;code&gt;Y.py&lt;/code&gt; the &lt;code&gt;import X&lt;/code&gt; line actually triggers a full run over &lt;code&gt;X&lt;/code&gt;.
This means that &lt;code&gt;X.call()&lt;/code&gt; is defined after all before it&amp;rsquo;s called in &lt;code&gt;Y&lt;/code&gt;!
Another detail we notice now is that &lt;code&gt;Y&lt;/code&gt; calls the module X; but when the executing of the main file continues, we are running the &lt;code&gt;__main__&lt;/code&gt; object.&lt;/p&gt;
&lt;p&gt;Now let&amp;rsquo;s see what happens when calling Y as the main function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; python Y&lt;span style="color:#f92672"&gt;.&lt;/span&gt;py
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Name: Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Y start
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Traceback (most recent call last):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;Y.py&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; X
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;C:\Users\Edwin Wenink\Documents\programming-practice\Python\circular_import\X.py&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Y
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; File &lt;span style="color:#e6db74"&gt;&amp;#34;C:\Users\Edwin Wenink\Documents\programming-practice\Python\circular_import\Y.py&amp;#34;&lt;/span&gt;, line &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, &lt;span style="color:#f92672"&gt;in&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;module&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; X&lt;span style="color:#f92672"&gt;.&lt;/span&gt;call()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;AttributeError&lt;/span&gt;: module &lt;span style="color:#e6db74"&gt;&amp;#39;X&amp;#39;&lt;/span&gt; has no attribute &lt;span style="color:#e6db74"&gt;&amp;#39;call&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, executing &lt;code&gt;Y&lt;/code&gt; triggers &lt;code&gt;import X&lt;/code&gt;.
This in turn, will trigger &lt;code&gt;import Y&lt;/code&gt;.
Because &lt;code&gt;Y&lt;/code&gt; is not in the list of modules yet (we haven&amp;rsquo;t run &lt;code&gt;import Y&lt;/code&gt;), Y will be compiled and we&amp;rsquo;ll encounter &lt;code&gt;X.call()&lt;/code&gt; before this function is defined.&lt;/p&gt;
&lt;p&gt;Things get even more complicated when &lt;code&gt;Y&lt;/code&gt; is also called from &lt;code&gt;X&lt;/code&gt;.
Even when you can postpone some imports to quickly fix the situation, it&amp;rsquo;s better to avoid this situation altogether by refactoring your code.
Otherwise, your code will break if a colleague wonders why there&amp;rsquo;s an import statement at the bottom of the file and moves it up.&lt;/p&gt;
&lt;p&gt;TLDR; &lt;em&gt;avoid circular imports!&lt;/em&gt;&lt;/p&gt;</description></item><item><title>Initializing nested lists correctly</title><link>https://www.edwinwenink.xyz/posts/67-initializing_nested_lists_correctly/</link><pubDate>Wed, 27 Jul 2022 11:16:50 +0200</pubDate><guid>https://www.edwinwenink.xyz/posts/67-initializing_nested_lists_correctly/</guid><description>&lt;p&gt;If you want to initialize a list with a certain size in Python, you can use the following clean syntax:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;]&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We can then fill the list with elements:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But watch what happens when we try to use the same syntax for declaring a 2D dynamic array:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [[]]&lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[], [], []]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The desired result here was the ragged array &lt;code&gt;[[], [1], []]&lt;/code&gt;, but instead we accidentally filled &lt;em&gt;all&lt;/em&gt; nested lists&amp;hellip;
What happened?!
Well, we observe here that the &lt;code&gt;*&lt;/code&gt; operator creates a list where the elements reference the same underlying object!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr2[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1474142748360&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr2[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1474142748360&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr2[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1474142748360&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;So that explains why adjusting one sublist affects all sublists: they are the same object.
But why did we then not have the same issue when initializing the flat empty list with None objects?
Actually, the &lt;code&gt;*&lt;/code&gt; operator works exactly the same here and &lt;em&gt;also&lt;/em&gt; creates a reference to the same object.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;140720689536224&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr[&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;140720689536224&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But if we inspect the element where we filled in a value, we &lt;em&gt;do&lt;/em&gt; see that it is a new object:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; id(arr[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;140720690012576&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The crucial difference is that this &lt;code&gt;NoneType&lt;/code&gt; object is &lt;em&gt;immutable&lt;/em&gt;.
The referenced object &lt;em&gt;cannot be changed&lt;/em&gt; but is rather replaced with a new object.
The same reasoning holds when we have a list of integers or strings.
In case of a list of lists, or a list of dictionaries (any mutable data structure) however, we &lt;em&gt;can&lt;/em&gt; adjust the referenced object and then the change will reflect onto all sublists.
Because something like &lt;code&gt;[1]*3&lt;/code&gt; works as expected, it can be hard to spot the difference in behavior when working with nested mutable data structures.&lt;/p&gt;
&lt;p&gt;If we explicitly replace a whole sublist with a new object, there&amp;rsquo;s no issue:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is not a practical solution though, because we want to be able to use functions like &lt;code&gt;append()&lt;/code&gt; on sublists correctly.
The general solution is to force Python to make a new object for each sublist, which means - however nice and clean the syntax looks - we have to avoid using &lt;code&gt;*&lt;/code&gt; for this!
Instead, create the sublists in an explicit loop or for example a list comprehension:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr3 &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [[] &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; _ &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[], [], []]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr3[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; arr3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[], [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;], []]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This way each of the sublists is its own object, rather than being a reference to the same list, because we force Python to evaluate &lt;code&gt;[]&lt;/code&gt; 3 times instead of only once.&lt;/p&gt;</description></item><item><title>Regular expressions with optional starting or ending groups</title><link>https://www.edwinwenink.xyz/posts/66-regex_with_starting_and_ending_optional_groups/</link><pubDate>Thu, 10 Feb 2022 10:29:34 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/66-regex_with_starting_and_ending_optional_groups/</guid><description>&lt;p&gt;I&amp;rsquo;m currently working on a classifier that can extract punishments from Dutch criminal law decisions.
One of those punishments is called &amp;ldquo;TBS&amp;rdquo;, which is assigned in severe cases where it is shown that the suspect was under the influence of a psychiatric condition at the time of committing the crime.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s two types of TBS in the Netherlands: with &amp;ldquo;verpleging&amp;rdquo; (mandatory psychiatric treatment) and with &amp;ldquo;voorwaarden&amp;rdquo; (several conditions).
We want to match &amp;ldquo;terbeschikkingstelling&amp;rdquo; (TBS), but if the type of TBS is specified, we want to capture that too.&lt;/p&gt;
&lt;p&gt;These TBS judgments occur in free natural language texts, but because lawyers and judges tend to use standard formulations with legal jargon &amp;ndash; although&amp;hellip; &amp;ldquo;standard&amp;rdquo;&amp;hellip; who really talks like that? &amp;ndash; we may try to extract information from case decisions using regular expressions.
Regular expressions are essentially a powerful way to do pattern matching on text.&lt;/p&gt;
&lt;h2 id="optional-group-after-greedy-quantifier"&gt;Optional group after greedy quantifier &lt;a href="#optional-group-after-greedy-quantifier"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To start, we want to match the following test strings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;de maatregel van terbeschikkingstelling wordt aan de verdachte opgelegd.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;de maatregel van terbeschikkingstelling met voorwaarden te gelasten.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&amp;ldquo;de maatregel van terbeschikkingstelling met verpleging van overheidswege te gelasten.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In these test cases, the type of TBS is mentioned at the end of the match and is optional.&lt;/p&gt;
&lt;p&gt;A fully naive first attempt to tackle this problem could be as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(terbeschikkingstelling).*(voorwaarden|verpleging)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But this will match &amp;ldquo;terbeschikkingstelling&amp;rdquo; but &lt;em&gt;not&lt;/em&gt; &amp;ldquo;verpleging&amp;rdquo; because of the &amp;ldquo;dot star soup&amp;rdquo; (expression I found and liked on rexegg.com).
Because the ending group is optional, &lt;code&gt;.*&lt;/code&gt; will consume until the end of the string and be happy.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve actually never used regular expressions outside of trivial situations, so I had to go back and study them a bit to find a better solution to my problem
(shout out to
&lt;a href="https://www.rexegg.com/" target="_blank"&gt;rexegg.com&lt;/a&gt;!).&lt;/p&gt;
&lt;p&gt;Essentially, we want the &amp;ldquo;dot star&amp;rdquo; to expand until we encounter &amp;ldquo;verpleging&amp;rdquo; or &amp;ldquo;voorwaarden&amp;rdquo;, but no further, and then capture &amp;ldquo;verpleging&amp;rdquo; or &amp;ldquo;voorwaarden&amp;rdquo;.
That is, we want to match any token (&lt;code&gt;.&lt;/code&gt;) that is &lt;em&gt;not followed&lt;/em&gt; by &amp;ldquo;verpleging&amp;rdquo; or &amp;ldquo;voorwaarden&amp;rdquo;, making these words essentially function as delimiters that restrict the scope of the greedy quantifier.
This is done with a &lt;em&gt;negative lookahead&lt;/em&gt;, which looks like this &lt;code&gt;(?!)&lt;/code&gt;.
This ensures that the pattern does &lt;em&gt;not&lt;/em&gt; occur after the position the regex engine is currently matching.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s first apply this idea to only one of the alternatives: &lt;code&gt;(?!voorwaarden).&lt;/code&gt;.
We want to repeat this zero or more times, so we wrap this in a non-capturing group and apply the greedy star quantifier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?:(?!voorwaarden).)*
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now the scope of the star is limited, because it will stop matching once &amp;ldquo;voorwaarden&amp;rdquo; is found.
In this case we actually want to capture &amp;ldquo;voorwaarden.&amp;rdquo;
Because we know the star will stop matching right before &amp;ldquo;voorwaarden&amp;rdquo;, we can safely gobble up &amp;ldquo;voorwaarden&amp;rdquo; as a literal match:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?:(?!voorwaarden).)*(voorwaarden)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this case, &amp;ldquo;voorwaarden&amp;rdquo; is still a required match.
But the crux is that now we can safely make the ending group optional, because we&amp;rsquo;ve scoped the greedy quantifier and prevented it from gobbling up our optional group at the end!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?:(?!voorwaarden).)*(voorwaarden)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that with an optional group at the end, we cannot make the star quantifier lazy (&lt;code&gt;*?&lt;/code&gt;) because then the regex will never try to find the optional ending group (yep, it&amp;rsquo;s &lt;em&gt;really&lt;/em&gt; lazy!).&lt;/p&gt;
&lt;p&gt;Now we finish up by including the second alternative.
The whole regex becomes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(terbeschikkingstelling)(?:(?!voorwaarden|verpleging).)*(voorwaarden|verpleging)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only thing left to do is to think a bit about what happens when &amp;ldquo;voorwaarden&amp;rdquo; or &amp;ldquo;verpleging&amp;rdquo; does &lt;em&gt;not&lt;/em&gt; occur in our input string.
We need to design for failure.
If the optional group is absent, the regex will always match until the end of the input string.
In my particular problem that&amp;rsquo;s quite bad, because I&amp;rsquo;m feeding the regex whole paragraphs of text at once.
We can use a bit of domain knowledge here though, because the further specification of the type of TBS will always occur in a short window after the main punishment is mentioned.
So an easy solution would be to explicitly specify the window in which we will look for the type specification, let&amp;rsquo;s say within 100 characters:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(terbeschikkingstelling)(?:(?!voorwaarden|verpleging).){0,100}(voorwaarden|verpleging)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The test cases will now return the following groups:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;de maatregel van terbeschikkingstelling wordt aan de verdachte opgelegd.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-&amp;gt; group 1: terbeschikkingstelling
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;de maatregel van terbeschikkingstelling met voorwaarden te gelasten.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-&amp;gt; group 1: terbeschikkingstelling; group 2: voorwaarden
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;#34;de maatregel van terbeschikkingstelling met verpleging van overheidswege te gelasten.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;-&amp;gt; group 1: terbeschikkingstelling, group 2: verpleging
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="diving-deeper-into-the-use-case"&gt;Diving deeper into the use case &lt;a href="#diving-deeper-into-the-use-case"&gt;¶&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s take some actual test cases where TBS is imposed in Dutch law:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;ldquo;gelast de terbeschikkingstelling van verdachte, met verpleging van overheidswege&amp;rdquo; (ECLI:NL:RBZWB:2020:6268).&lt;/li&gt;
&lt;li&gt;&amp;ldquo;gelast dat de verdachte, voor de feiten 2, 3 en 4, ter beschikking wordt gesteld en stelt daarbij de volgende, het gedrag van de ter beschikking gestelde betreffende, voorwaarden&amp;rdquo; (ECLI:NL:RBLIM:2020:9778).&lt;/li&gt;
&lt;li&gt;&amp;ldquo;De rechtbank verlengt de termijn van de terbeschikkingstelling van veroordeelde met één jaar&amp;rdquo; (ECLI:NL:RBNNE:2020:4558).&lt;/li&gt;
&lt;li&gt;&amp;ldquo;verlengt de termijn gedurende welke [verdachte] ter beschikking is gesteld met verpleging van overheidswege met één jaar&amp;rdquo; (ECLI:NL:RBLIM:2020:10468).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We first recognizes that there are alternative formulations like &amp;ldquo;ter beschikking is gesteld&amp;rdquo; and &amp;ldquo;ter beschikking wordt gesteld,&amp;rdquo; so we adjust the regex for that.
We also allow &amp;ldquo;terbeschikkingstelling&amp;rdquo; to be written as &amp;ldquo;ter beschikking stelling&amp;rdquo; and include &amp;ldquo;TBS&amp;rdquo; as the relevant abbreviation.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(TBS|terbeschikkingstelling|ter beschikking (?:wordt |is )?(?:stelling|gesteld))(?:(?!voorwaarden|verpleging).){0,100}(voorwaarden|verpleging)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now, there is a subtlety: legal jargon related to &amp;ldquo;ter beschikking stellen&amp;rdquo; does not necessarily indicate TBS but can also relate e.g. to goods.
If we really want to make sure these phrases relates to TBS (i.e. avoid false positives) we should probably make the ending group non-optional after all.
However, this means we do not match cases where TBS is assigned in the past, but is now prolongated such as in &amp;ldquo;verlengt de termijn van de terbeschikkingstelling.&amp;rdquo;
The type of TBS is not specified here because it has already been determined in a previous judgement.
So our new problem statement could be: we think a TBS-punishment is assigned either when it is preceded by an indication of prolongation such as &amp;ldquo;verlenging&amp;rdquo; or when the type of TBS is explicitly specified (with &amp;ldquo;voorwaarden&amp;rdquo; or &amp;ldquo;verpleging&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s again decompose the problem and solve the case where &amp;ldquo;verlenging&amp;rdquo; occurs &lt;em&gt;before&lt;/em&gt; the indication of TBS.
We again want to design a delimiter, but now one that determines where to &lt;em&gt;start&lt;/em&gt; matching instead of where to end.
We can express that we only want to start matching after having seen either &amp;ldquo;verlenging&amp;rdquo; or &amp;ldquo;verlengt&amp;rdquo; with a &lt;em&gt;positive lookbehind&lt;/em&gt; on &amp;ldquo;verleng&amp;rdquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?&amp;lt;=verleng).*?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But since we know where to begin matching and we&amp;rsquo;d like to also capture &amp;ldquo;verlenging&amp;rdquo;, we can just anchor the start with a literal match:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?P&amp;lt;verlenging&amp;gt;verlengt|verlenging).{0,50}(?P&amp;lt;TBS1&amp;gt;TBS|terbeschikkingstelling|ter beschikking (?:wordt |is )?(?:stelling|gesteld))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Combining everything we get a quite lengthy regex with two alternatives.
Either we require something like &amp;ldquo;verlenging&amp;rdquo; in front of the regex, or something like &amp;ldquo;veroordeling&amp;rdquo; or &amp;ldquo;voorwaarden&amp;rdquo; after.
The ending group is now no longer optional:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?P&amp;lt;verlenging&amp;gt;verlengt|verlenging).{0,50}(TBS|terbeschikkingstelling|ter beschikking (?:wordt |is )?(?:stelling|gesteld))|(TBS|terbeschikkingstelling|ter beschikking (?:wordt |is )?(?:stelling|gesteld))(?:(?!voorwaarden|verpleging).){0,100}(voorwaarden|verpleging)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;By using this alternation, we have to repeat the regex for the TBS part.
I also find this a bit annoying, because if I want to do something with the &amp;ldquo;TBS&amp;rdquo; part downstream it can either be in the second or third capture group.
On average, this also increases the number of steps the regex engine has to traverse.&lt;/p&gt;
&lt;p&gt;We can also change our mindset: instead of only matching what we want to keep, we can capture all relevant components and throw away matches we don&amp;rsquo;t want downstream.
For example, we can get rid of the alternation and just have optional groups both at the beginning and end.
The only thing we then have to do is filter out matches that have neither of the optional groups.&lt;/p&gt;
&lt;p&gt;The regex with two optional groups, both at the beginning and the end, could look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(?:(verlengt|verlenging).{0,50})?(TBS|terbeschikkingstelling|ter beschikking (?:wordt |is )?(?:stelling|gesteld))(?:(?!voorwaarden|verpleging).){0,100}(voorwaarden|verpleging)?
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test case 1: &amp;ldquo;gelast de terbeschikkingstelling van verdachte, met verpleging van overheidswege&amp;rdquo; (ECLI:NL:RBZWB:2020:6268).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;match: terbeschikkingstelling van verdachte, met verpleging
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 2: terbeschikkingstelling
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 3: verpleging
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test case 2: &amp;ldquo;gelast dat de verdachte, voor de feiten 2, 3 en 4, ter beschikking wordt gesteld en stelt daarbij de volgende, het gedrag van de ter beschikking gestelde betreffende, voorwaarden&amp;rdquo; (ECLI:NL:RBLIM:2020:9778).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;match: ter beschikking wordt gesteld en stelt daarbij de volgende, het gedrag van de ter beschikking gestelde betreffende, vooraarden
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 2: ter beschikking wordt gesteld
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 3: voorwaarden
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test case 3: &amp;ldquo;De rechtbank verlengt de termijn van de terbeschikkingstelling van veroordeelde met één jaar&amp;rdquo; (ECLI:NL:RBNNE:2020:4558).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;match: verlengt de termijn van de terbeschikkingstelling van veroordeelde met één jaar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 1: verlengt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 2: terbeschikkingstelling
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Test case 4: &amp;ldquo;verlengt de termijn gedurende welke [verdachte] ter beschikking is gesteld met verpleging van overheidswege met één jaar&amp;rdquo; (ECLI:NL:RBLIM:2020:10468).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;match: verlengt de termijn gedurende welke [verdachte] ter beschikking is gesteld met verpleging
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 1: verlengt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 2: ter beschikking is gesteld
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;group 3: verpleging
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Some final notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In this setup, not making &lt;code&gt;(voorwaarden|verpleging)?&lt;/code&gt; optional leads to large inefficiency if the group is not in the string. It will cause the lookahead to be repeated a lot in an attempt to still find the group.&lt;/li&gt;
&lt;li&gt;Downstream we may opt to reject the match if neither of the optional groups is matched, because this may be a false positive. The upside is that this gives flexibility in your application without having to redesign the regex. As we see in the last test case, it may also be that &lt;em&gt;both&lt;/em&gt; groups are present as we see in test case 4.&lt;/li&gt;
&lt;li&gt;There are other edge cases to catch for detecting TBS. I only consider a few test cases to keep things simple.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Please let me know if you see points where I can improve (e.g. in terms of optimization)!&lt;/p&gt;
&lt;p&gt;Related note:
&lt;a href="./zettelkasten/index_regex/"&gt;index regex&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Stemming and lemmatizing with sklearn vectorizers</title><link>https://www.edwinwenink.xyz/posts/65-stemming_and_lemmatizing_with_sklearn_vectorizers/</link><pubDate>Fri, 21 Jan 2022 19:21:21 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/65-stemming_and_lemmatizing_with_sklearn_vectorizers/</guid><description>&lt;p&gt;One of the most basic techniques in Natural Language Processing
&lt;a href="./zettelkasten/index_nlp_natural_language_processing/"&gt;(NLP)&lt;/a&gt; is the creation of feature vectors based on word counts.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;scikit-learn&lt;/code&gt; provides efficient classes for this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; sklearn.feature_extraction.text &lt;span style="color:#f92672"&gt;import&lt;/span&gt; CountVectorizer, TfidfVectorizer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If we want to build feature vectors over a vocabulary of &lt;em&gt;stemmed&lt;/em&gt; or &lt;em&gt;lemmatized&lt;/em&gt; words, how can we do this and still benefit from the ease and efficiency of using these &lt;code&gt;sklearn&lt;/code&gt; classes?&lt;/p&gt;
&lt;h2 id="vectorizers-the-basic-use-case"&gt;Vectorizers: the basic use case &lt;a href="#vectorizers-the-basic-use-case"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Conceptually, these vectorizers first build up the vocabulary of your whole text corpus.
The size of the vocabulary determines the length of the feature vectors, unless you specify a maximum amount of features (which you probably should, cf.
&lt;a href="./zettelkasten/202009151217-zipfs_law/"&gt;Zipfs law&lt;/a&gt;).
The vectorizers check for each document how often a certain word (or n-gram, technically) occurs in that document.
The &lt;code&gt;CountVectorizer&lt;/code&gt; only takes the term frequency (TF) into account.
However, words that occur in almost all the documents (like stop words) are not very useful for characterizing individual documents and distinguishing them from others.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We should treat matches on non-frequent terms as more valuable than ones on frequent terms, without disregarding the latter altogether. The natural solution is to correlate a term&amp;rsquo;s matching value with its collection frequency. (Karen Spärk Jones, 1972)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;TfidfVectorizer&lt;/code&gt; therefore additionally weighs the word frequency with how common the word is in the whole corpus.
If a word occurs a lot in document $t$ but is quite rare throughout the whole corpus, then this is a useful word to characterize the current document.
Conversely, if a term is frequent in document $t$ but it occurs a lot in literally every other document as well, then it is a poor descriptor.
In its most basic form (without smoothing etc.)
&lt;a href="./zettelkasten/202201211817-tf_idf/"&gt;TF*IDF scoring&lt;/a&gt; looks like this:&lt;/p&gt;
&lt;p&gt;$$TF(t,d) * log (\frac{N}{DF(t)})$$&lt;/p&gt;
&lt;p&gt;Where $TF(t,d)$ is the frequency of term $t$ in document $d$, $N$ is the total amount of documents in the corpus, and $DF(t)$ is the amount of documents in which term $t$ occurs.
The logarithm is called the Inverse Document Frequency (IDF), hence we get TF*IDF.
The logarithm prevents that very rare words completely dominate the score.
Additionally, it punishes the most frequent words relatively heavy.&lt;/p&gt;
&lt;h2 id="composing-a-new-tokenizer"&gt;Composing a new tokenizer &lt;a href="#composing-a-new-tokenizer"&gt;¶&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;It is very convenient and efficient to use the &lt;code&gt;sklearn&lt;/code&gt; vectorizers, but how can we use them when we want to do additional natural language processing during the building of the corpus vocabulary?&lt;/p&gt;
&lt;p&gt;Vectorizers can be customised with three arguments: 1) preprocessor, 2) tokenizer, and 3) analyzer:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The preprocessor is a callable that operates on a whole string and returns a whole string.&lt;/li&gt;
&lt;li&gt;The tokenizer takes the preprocessor output and returns a list of tokens.&lt;/li&gt;
&lt;li&gt;The analyzer is a callable that replaces the whole pipeline, including preprocessing and tokenization, and I think also including N-gram extraction and stop word filtering.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So in order to add stemming or lemmatization to the sklearn vectorizers, a good approach is to include this in a custom tokenize function.
This does assume our stemming and lemmatization functions only need access to tokens, instead of the whole input strings (may be documents, sections, paragraphs, sentences etc.).&lt;/p&gt;
&lt;p&gt;This is a very nice snippet to compose functions using &lt;code&gt;functools&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; functools
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;compose&lt;/span&gt;(&lt;span style="color:#f92672"&gt;*&lt;/span&gt;functions):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;&amp;#39;&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Compose an arbitary amount of functions into a single function
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; Source: https://mathieularose.com/function-composition-in-python
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;comp&lt;/span&gt;(f, g):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;lambda&lt;/span&gt; x: f(g(x))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; functools&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reduce(comp, functions, &lt;span style="color:#66d9ef"&gt;lambda&lt;/span&gt; x: x)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Assuming we have some class where we can assign a stemmer, a lemmatizer, or neither, we can override the tokenizer as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# If a stemmer or lemmatizer is provided in the configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# compose a new tokenization function that includes stemming/lemmatization after tokenization.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# This allows stemming or lemmatization to be integrated e.g. with CountVectorizer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; stemmer:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenize &lt;span style="color:#f92672"&gt;=&lt;/span&gt; compose(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_stemmer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stem, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenizer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tokenize)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;elif&lt;/span&gt; lemmatizer:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenize &lt;span style="color:#f92672"&gt;=&lt;/span&gt; compose(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_lemmatizer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lemmatize, self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenizer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tokenize)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenize &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenizer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tokenize
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Note that the order of the composition matters, because the function signatures differ.
A tokenization function takes a string as an input and outputs a list of tokens, and our stemming or lemmatization function then operates on this list of tokens.
We can now define a &lt;code&gt;TfidfVectorizer&lt;/code&gt; with our custom callable!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ngram_range &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;max_features &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;use_idf &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tfidf &lt;span style="color:#f92672"&gt;=&lt;/span&gt; TfidfVectorizer(tokenizer&lt;span style="color:#f92672"&gt;=&lt;/span&gt;self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;_tokenize,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_features&lt;span style="color:#f92672"&gt;=&lt;/span&gt;max_features,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ngram_range&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ngram_range,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; min_df&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_df&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; use_idf&lt;span style="color:#f92672"&gt;=&lt;/span&gt;use_idf)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The vocabulary will now consist of stems and lemmas.&lt;/p&gt;</description></item><item><title>Applying operations on grouped dataframes in Pandas</title><link>https://www.edwinwenink.xyz/posts/64-apply_operations_on_grouped_objects_pandas/</link><pubDate>Tue, 04 Jan 2022 13:41:17 +0100</pubDate><guid>https://www.edwinwenink.xyz/posts/64-apply_operations_on_grouped_objects_pandas/</guid><description>&lt;p&gt;I have the following use case:
I have legal text data that is stored on section level, so a single document with multiple sections will provide multiple rows to the data set.
These sections have a particular type.
For example, a case is typically concluded with a section where the judges offer their final ruling.&lt;/p&gt;
&lt;p&gt;I want to investigate the hypothesis that each case has indeed a single section of the type &amp;lsquo;decision&amp;rsquo;.&lt;/p&gt;
&lt;p&gt;A dummy dataframe for this situation looks may like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#39;doc_id&amp;#39;&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;section_id&amp;#39;&lt;/span&gt;: [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#39;other&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;decision&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;other&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;other&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;decision&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;decision&amp;#39;&lt;/span&gt;]}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(data)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This gives:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; df
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; doc_id section_id type
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; other
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; decision
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; other
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; other
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; decision
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; decision
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This dummy example distinguishes three cases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the first document contains a single section with a decision, as expected&lt;/li&gt;
&lt;li&gt;the second document contains no section with a decision&lt;/li&gt;
&lt;li&gt;the third document contains two sections with a decision&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Notice that in this case we cannot simply test our hypothesis by counting the amount of documents and checking equality with the number of sections with type &amp;lsquo;decision&amp;rsquo;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; len(df[&lt;span style="color:#e6db74"&gt;&amp;#39;doc_id&amp;#39;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unique())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; len(df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[df[&lt;span style="color:#e6db74"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;decision&amp;#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The totals add up, but our hypothesis is clearly false!&lt;/p&gt;
&lt;p&gt;Instead, we want to test our hypothesis on the level of documents, not sections, so we &lt;em&gt;group&lt;/em&gt; our data by the document identifier:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#39;doc_id&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;pandas&lt;span style="color:#f92672"&gt;.&lt;/span&gt;core&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby&lt;span style="color:#f92672"&gt;.&lt;/span&gt;generic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrameGroupBy object at &lt;span style="color:#ae81ff"&gt;0x000001C55EF8F9E8&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we want to count the number of &amp;lsquo;decision&amp;rsquo; sections on the data that is grouped &lt;em&gt;per document&lt;/em&gt;, so we want to &lt;em&gt;apply&lt;/em&gt; the counting operation on the grouped data.
For this I apply a lambda expression in order to only regard data from the &amp;rsquo;type&amp;rsquo; column of each group.
With this functional style, we can do all operations in a single line:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; decision_counts &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#39;doc_id&amp;#39;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;apply(&lt;span style="color:#66d9ef"&gt;lambda&lt;/span&gt; x: len(x&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[x[&lt;span style="color:#e6db74"&gt;&amp;#39;type&amp;#39;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;decision&amp;#39;&lt;/span&gt;]))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; decision_counts
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;doc_id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We end up with a dataframe that lists the number of &amp;lsquo;decision&amp;rsquo; sections per document.
Counting how many documents violate our hypothesis is now trivial.
We can count the number of documents that have no &amp;lsquo;decision&amp;rsquo; sections and those that have more than one, as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; decision_counts[decision_counts &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;doc_id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; decision_counts[decision_counts &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;doc_id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We indeed see that document 2 has zero &amp;lsquo;decision&amp;rsquo; sections, whereas document 3 has two.&lt;/p&gt;</description></item></channel></rss>