<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://martibosch.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://martibosch.github.io/" rel="alternate" type="text/html" /><updated>2026-10-07T10:43:56+00:00</updated><id>https://martibosch.github.io/feed.xml</id><title type="html">Martí Bosch</title><subtitle>AI weather models, urban climate, Python and a bit of landscape ecology</subtitle><entry><title type="html">Thoughts after the CI2026 Hackathon: on good practices for deep learning pipelines</title><link href="https://martibosch.github.io/ci2026-good-practices/" rel="alternate" type="text/html" title="Thoughts after the CI2026 Hackathon: on good practices for deep learning pipelines" /><published>2026-05-08T00:00:00+00:00</published><updated>2026-05-08T00:00:00+00:00</updated><id>https://martibosch.github.io/ci2026-good-practices</id><content type="html" xml:base="https://martibosch.github.io/ci2026-good-practices/"><![CDATA[<p>I finally managed to “win” the <a href="https://tobifinn.github.io/ci_hackathon_26">Climate informatics 26 Hackathon</a> on using deep learning to predict cloud cover. Too bad this happened only:</p>

<ul>
  <li>one week after the final submission deadline,</li>
  <li>after being able to see <a href="https://github.com/radiradev/1st-place-solution-CI2026">the actual winning solution</a>,</li>
  <li>spending half of my Claude Pro weekly limits, and</li>
  <li>spending some extra 25$ from my pocket on <a href="https://developer.nvidia.com/brev">NVIDIA Brev</a>.</li>
</ul>

<p>But let us focus on the positive side: what did I learn? I would say a lot. As someone relatively new to deep learning but relatively experienced in scientific Python (or at least I would like to think so), I was very curious to see how the “good practices” that I have learned over the years for computational pipelines that I run locally apply to deep learning pipelines that run on GPU servers. Here are some thoughts.</p>

<h2 id="the-ci2026-starter-kit">The CI2026 starter kit</h2>

<p>All the participants started from <a href="https://github.com/tobifinn/CI2026-StarterKit">a base repository set up by Tobias Finn</a> (a big shout-out for the organization) with the following structure:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CI2026-StarterKit/
+-- configs/                  # Hydra settings
|   +-- data/                 # dataset paths and splits
|   +-- experiments/          # baseline experiment presets
|   +-- model/                # architecture hyperparameters
|   +-- suite/                # validation/test forecast suites
|   +-- test_data/            # test-set paths
|   +-- train.yaml            # default training config
|   +-- forecast.yaml         # default forecast config
|   +-- submit.yaml           # default submission config
+-- data/                     # local runtime data, not committed
|   +-- train_data/           # downloaded inputs and targets
|   +-- models/               # checkpoints and logs
|   +-- forecasts/            # generated netCDF predictions
+-- notebooks/                # exploration and analysis
+-- scripts/                  # Hydra entry points
|   +-- train.py              # train a model
|   +-- forecast.py           # generate forecasts
|   +-- evaluate.py           # score validation forecasts
|   +-- submit.py             # forecast and submit predictions
+-- starter_kit/              # installable Python package
    +-- data.py               # PyTorch datasets
    +-- layers.py             # input normalisation
    +-- model.py              # shared training interface
    +-- baselines/            # reference model implementations
        +-- mlp.py
        +-- parametric.py
        +-- sundquist.py
</code></pre></div></div>

<p>This is indeed a structure that I have seen quite often in GitHub but I had never actually used myself, essentially because it is designed to run on a GPU. Instead, I have sticked to <a href="https://github.com/martibosch/cookiecutter-data-snake">my personal flavour</a> of the <a href="https://cookiecutter-data-science.drivendata.org">cookiecutter-data-science</a> template and my GPU requirements have been fullfilled <em>serverlessly</em> by writing modal apps, e.g., to <a href="https://github.com/martibosch/aifs-modal">run AIFS forecasts</a>, or to <a href="https://github.com/martibosch/deepforest-modal-app">run training/inference steps in computer vision</a>. But we’ll get to that later — for this workshop, we had 100$ of <a href="https://brev.nvidia.com">Brev</a> credits (graciously offered by Nvidia) to run on NVIDIA L4 GPUs throughout the hackathon, so we did not have to worry about GPU cost.</p>

<h2 id="snakemake-my-dearest-friend">Snakemake, my dearest friend</h2>

<p>After launching the provided Brev environment, accessing it through JupyterLab and setting up conda environment, the first step of the computational pipeline was to download (and unzip) <a href="https://huggingface.co/datasets/tobifinn/CI2026Hackathon">the input dataset from HuggingFace</a>. Then, we copied and pasted a few of the commands to run <a href="https://github.com/tobifinn/CI2026-StarterKit#training">training</a>, <a href="https://github.com/tobifinn/CI2026-StarterKit#forecasting">inference</a> and <a href="https://github.com/tobifinn/CI2026-StarterKit#evaluation">evaluation</a> with the provided baseline multi-layered perceptron (MLP) model:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># for the baseline MLP, per Hydra defaults</span>
python scripts/train.py <span class="nv">n_epochs</span><span class="o">=</span>50 <span class="nv">learning_rate</span><span class="o">=</span>5e-4 <span class="nv">device</span><span class="o">=</span>cuda
python scripts/forecast.py <span class="nv">device</span><span class="o">=</span>cuda
python scripts/evaluate.py <span class="se">\</span>
    <span class="nt">--prediction_dir</span> data/forecasts/baseline_mlp <span class="se">\</span>
    <span class="nt">--to_json</span> <span class="se">\</span>
    <span class="nt">--output_path</span> scores/baseline_mlp.json
</code></pre></div></div>

<p>as well as with the physical reference baseline following Sundqvist et al. (1989)<sup id="fnref:sundqvist" role="doc-noteref"><a href="#fn:sundqvist" class="footnote" rel="footnote">1</a></sup></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python scripts/train.py +experiments<span class="o">=</span>baseline_sundquist <span class="nv">device</span><span class="o">=</span>cuda
python scripts/forecast.py +experiments<span class="o">=</span>baseline_sundquist <span class="nv">device</span><span class="o">=</span>cuda
python scripts/evaluate.py <span class="nt">--prediction_dir</span> data/forecasts/baseline_sundquist
python scripts/evaluate.py <span class="se">\</span>
    <span class="nt">--prediction_dir</span> data/forecasts/baseline_sundquist <span class="se">\</span>
    <span class="nt">--to_json</span> <span class="se">\</span>
    <span class="nt">--output_path</span> scores/baseline_sundquist.json
</code></pre></div></div>

<p>However, it only took me copy-pasting these commands to mess up and overwrite the MLP scores with its Sundqvist by forgetting to update <code class="language-plaintext highlighter-rouge">--output_path</code>. At that point, it immediately occured to me that I usually lean on Snakemake to avoid these mistakes, so why shouldn’t I do the same here? Enter Snakemake.</p>

<p>Since Hydra already takes care of managing paths for the train and forecast parts, the main gap was the <code class="language-plaintext highlighter-rouge">scripts/evaluate.py</code> step, where <code class="language-plaintext highlighter-rouge">--output_path</code> had to be specified manually. Adding a thin Snakemake wrapper around all three steps meant that paths flow automatically from the <code class="language-plaintext highlighter-rouge">{run}</code> wildcard through the whole chain via a directed acyclic graph (DAG): the right commands run in the right order and already-completed steps are skipped on re-runs. We also added the data download and unzip steps so that the full pipeline — from raw dataset to evaluation scores — could be expressed as a single snakemake call. Furthermore, region expansion, which would otherwise require looping over the four ERA5 and AIMIP domains by hand, became a one-liner with <code class="language-plaintext highlighter-rouge">expand()</code>. Convenience targets like <code class="language-plaintext highlighter-rouge">all</code> and <code class="language-plaintext highlighter-rouge">test_forecasts</code> replaced sequences like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>region <span class="k">in </span>era5_region1 era5_region2 aimip_region1 aimip_region2<span class="p">;</span> <span class="k">do
    </span>python scripts/forecast.py +experiments<span class="o">=</span>baseline_mlp <span class="se">\</span>
        +test_data<span class="o">=</span>val_<span class="k">${</span><span class="nv">region</span><span class="k">}</span> <span class="nv">device</span><span class="o">=</span>cuda
<span class="k">done
</span>python scripts/evaluate.py <span class="se">\</span>
    <span class="nt">--prediction_dir</span> data/forecasts/baseline_mlp <span class="se">\</span>
	<span class="nt">--to_json</span> <span class="se">\</span>
	<span class="nt">--output_path</span> scores/baseline_mlp.json
</code></pre></div></div>

<p>with a single call:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>snakemake <span class="nt">--config</span> <span class="nv">runs</span><span class="o">=</span>baseline_mlp <span class="nt">-j1</span> <span class="nt">--resources</span> <span class="nv">gpu</span><span class="o">=</span>1
</code></pre></div></div>

<p>The checkpoint and forecast paths (e.g., <code class="language-plaintext highlighter-rouge">data/models/{run}/best_model.ckpt</code>) are declared in both Snakemake (for DAG construction) and in the Hydra configs (for runtime resolution). This is unavoidable: Snakemake must know file paths statically to determine what to run and what to skip. Nonetheless, in practice the overlap is small and the convention is simple enough that keeping both synchronized is a one-line change.</p>

<p>The value of adding a Snakefile did not stop there though: we realized that reference values for normalization <a href="https://github.com/tobifinn/CI2026-StarterKit/blob/main/starter_kit/baselines/mlp.py#L31-L43">were pre-computed from the training data and hardcoded to <code class="language-plaintext highlighter-rouge">mlp.py</code></a>, so in order to add further <a href="https://unidata.github.io/MetPy/latest/api/generated/metpy.calc.html">meteorological calculations</a> as derived input features (computed on-the-fly in the forward pass), we needed to add the computation of reference values to the pipeline with a new <code class="language-plaintext highlighter-rouge">compute_normalization</code> rule. The overall pipeline at this point looked as follows:</p>

<pre><code class="language-mermaid">flowchart TB
    download_data(download_data)
    data_ready["data/train_data/.download_complete"]
    compute_norm(compute_normalization)
    norm_stats["data/stats/normalization_{...}.json"]
    train(train)
    ckpt["data/models/{run}/best_model.ckpt"]

    subgraph forecasts["forecast for each split and region"]
        direction TB
        forecast_val(forecast)
        val_forecasts["val_{region}.nc&lt;br/&gt;for the four regions"]
        forecast_test(forecast)
        test_forecasts["test_{region}.nc&lt;br/&gt;for the four regions"]
    end

    evaluate(evaluate)
    score["scores/{run}.json"]
    all_target["all / val_scores"]
    test_target["test_forecasts"]

    download_data --&gt; data_ready --&gt; compute_norm --&gt; norm_stats --&gt; train --&gt; ckpt
    data_ready --&gt; train
    ckpt --&gt; forecast_val --&gt; val_forecasts
    ckpt --&gt; forecast_test --&gt; test_forecasts
    val_forecasts --&gt; evaluate --&gt; score --&gt; all_target
    test_forecasts --&gt; test_target

    classDef data fill:$DATA_FILL,stroke:$DATA_STROKE,color:$NODE_TEXT;
    classDef task fill:$TASK_FILL,stroke:$TASK_STROKE,color:$NODE_TEXT;
    classDef gpu fill:$GPU_FILL,stroke:$GPU_STROKE,stroke-width:2px,color:$NODE_TEXT;
    classDef target fill:$TARGET_FILL,stroke:$TARGET_STROKE,color:$NODE_TEXT;
    class data_ready,ckpt,val_forecasts,test_forecasts,score,norm_stats data;
    class train,forecast_val,forecast_test gpu;
    class download_data,evaluate,compute_norm task;
    class all_target,test_target target;
    style forecasts fill:$SUB_GPU_FILL,stroke:$SUB_GPU_STROKE,stroke-width:2px
</code></pre>

<h2 id="interlude-our-submission">Interlude: our submission</h2>

<p>I am not going to develop much on our submitted network because we did not win. My colleague was right to suggest using a U-Net<sup id="fnref:unet-claude" role="doc-noteref"><a href="#fn:unet-claude" class="footnote" rel="footnote">2</a></sup>, to which we added sphere-aware padding (circular along the longitude axis, replicate along latitude) and added relative humidity as a derived input feature computed on-the-fly in the forward pass alongside the raw pressure-level fields and two static auxiliary fields (land-sea mask and orography). The best results network were obtained with a wide configuration (96 base channels, 3 downsampling stages).</p>

<p>From my experience in computer vision, I actually thought of including augmentation-like transforms to our training loop, but discarded it since I considered that these would disregard the semantics of the land-sea mask and orography. However, in retrospective, the most likely reason we fell short was actually including those static geographic fields. By seeing the land-sea mask and orography during training, the network learned to associate cloud patterns with specific landscape features of the training region rather than the underlying atmospheric dynamics, and those associations do not transfer to a held-out region with different geography.</p>

<p>The winning solution description actually pointed at this: a geography-agnostic design that drops all static inputs and relies solely on pressure-level fields generalizes across regions by construction.</p>

<h2 id="comparing-experiments">Comparing experiments</h2>

<p>The starter kit provided logged the training loss at each epoch to a CSV file of the form <code class="language-plaintext highlighter-rouge">data/models/&lt;exp_name&gt;/train_log.csv</code>. In order to better compare experiments, we set up integration with <a href="https://wandb.ai">Weights &amp; Biases</a>. We could have also used <a href="https://github.com/mlflow/mlflow">MLflow</a> but I could not find a free hosted solution which allowed multiple collaborators — something included within the <a href="https://wandb.ai/site/pricing">free academic plan of Weights &amp; Biases</a>.</p>

<p>In any case, besides our submitted <code class="language-plaintext highlighter-rouge">unet_brute_xlarge</code> configuration, I added two additional experiments <em>a posteriori</em>. The first is <code class="language-plaintext highlighter-rouge">geounet_wide_rh_hilr</code>, a geography-agnostic UNet variant based ont the winning solution, to which I added on-the-fly relative humidity to the forward pass and set a high learning rate (0.001). The second is a (vibe-coded) SegFormer-B2 variant <code class="language-plaintext highlighter-rouge">segformer_b2_rh</code>, i.e., a transformer-based architecture that replaces the CNN backbone with a Mix Transformer encoder and a lightweight MLP decoder. The following validation loss curves from Weights &amp; Biases summarize the comparison:</p>

<p><img src="/assets/images/ci2026/wandb-val-loss.svg" alt="Validation loss curves" title="Validation loss curves" /></p>

<p>The <code class="language-plaintext highlighter-rouge">segformer_b2_rh</code> setup performed very poorly: its pre-training on natural images (ImageNet) and classification-oriented decoder were likely not the best choice. I may further add a U-Net with a Swin Transformer encoder, since its shifted-window attention is probably better to capture local spatial structure.</p>

<p>I also vibe-coded <a href="https://github.com/martibosch/CI2026-StarterKit/blob/main/notebooks/visualize_inputs_and_predictions.ipynb">a notebook to visualize the outputs of each model</a>:</p>

<p><img src="/assets/images/ci2026/model-comparison.png" alt="Model comparison" title="Model comparison" /></p>

<p>and <a href="https://github.com/martibosch/CI2026-StarterKit/blob/main/notebooks/normalization_analysis.ipynb">another notebook to inspect the distribution of each physical variable accross pressure levels</a> and potentially identify improvements to the z-score normalization — although some preliminary experiments suggest that these may make very little effect.</p>

<h2 id="on-the-overall-development-workflow-another-case-for-serverless-computer">On the overall development workflow: another case for serverless computer?</h2>

<p>The setup allowed editing code directly in Brev using JupyterLab. However, as a long time emacs user, that was not my preferred approach<sup id="fnref:emacs-jupyter" role="doc-noteref"><a href="#fn:emacs-jupyter" class="footnote" rel="footnote">3</a></sup>. Additionally, if I am not mistaken, only one user could access JupyterLab at a time, so we ended up adopting this workflow:</p>

<pre><code class="language-mermaid">flowchart TB
    subgraph Local["Local laptop"]
        direction TB
        local_edit["Edit code, configs and workflows"]
        local_push["Commit and push changes"]
        local_refine["Refine code or configuration"]
    end

    subgraph GitHub["GitHub"]
        direction TB
        github_repo[("Repository")]
    end

    subgraph Brev["Brev GPU instance"]
        direction TB
        brev_pull["Pull latest changes"]
        subgraph BrevLoop["Experiment loop"]
            direction TB
            brev_run["Run training, forecasts and evaluation"]
            brev_inspect["Inspect results"]
            brev_update["Pull update"]
            brev_rerun["Re-run GPU scripts"]
        end
    end

    local_edit --&gt; local_push --&gt; github_repo --&gt; brev_pull
    brev_pull --&gt; brev_run --&gt; brev_inspect --&gt; local_refine
    local_refine --&gt; local_push
    github_repo --&gt; brev_update --&gt; brev_rerun --&gt; brev_inspect

    classDef local fill:$TASK_FILL,stroke:$TASK_STROKE,color:$NODE_TEXT;
    classDef github fill:$TARGET_FILL,stroke:$TARGET_STROKE,color:$NODE_TEXT;
    classDef gpu fill:$GPU_FILL,stroke:$GPU_STROKE,stroke-width:2px,color:$NODE_TEXT;
    class local_edit,local_push,local_refine local;
    class github_repo github;
    class brev_pull,brev_run,brev_inspect,brev_update,brev_rerun gpu;
    style Local fill:$SUB_TASK_FILL,stroke:$SUB_TASK_STROKE
    style GitHub fill:$SUB_NEUTRAL_FILL,stroke:$SUB_NEUTRAL_STROKE
    style Brev fill:$SUB_GPU_FILL,stroke:$SUB_GPU_STROKE,stroke-width:3px
    style BrevLoop fill:$SUB_GPU_FILL,stroke:$SUB_GPU_STROKE,stroke-width:2px
</code></pre>

<p>The positive part of this set up is that all changes need to go through git, which can enforce good practices by means of pre-commit hooks and potentially continuous integration (CI). The bad part is that the GPU was idle most of the time. In fact, after setting up the Brev instance on the first day of the hackathon, my colleague and I went for lunch and realized that about $2 of credits were gone becuse we simply did not “Stop” the Brev deployment.</p>

<p>Although the training of the heaviest models could take about 2 hours to complete on the L40 GPU, iterative development of Python and yaml files largely dominated our overall time to solution. Using a GPU server for that not only is overkill but also inconveniennt as it hampers the good practices provided by LSP features in integrated development environment (IDEs) — especially for Emacs users such as myself. I thus believe that this is another great use case for a <em>serverless</em> setup:</p>

<pre><code class="language-mermaid">flowchart TB
    subgraph Local["Local laptop"]
        direction TB
        local_edit["Edit code, configs and workflows"]
        local_push["Commit and push changes"]
        subgraph LocalLoop["Experiment loop"]
            direction TB
            local_submit["Submit GPU job from local CLI"]
            local_inspect["Inspect results"]
            local_refine["Refine code or configuration"]
        end
    end

    subgraph GitHub["GitHub"]
        direction TB
        github_repo[("Repository")]
    end

    subgraph GPU["Serverless GPU"]
        direction TB
        gpu_run["Run training, forecasts and evaluation"]
    end

    local_edit --&gt; local_push --&gt; github_repo
    local_push --&gt; local_submit --&gt; gpu_run
    gpu_run --&gt;|"logs and artifacts"| local_inspect
    local_inspect --&gt; local_refine --&gt; local_push

    classDef local fill:$TASK_FILL,stroke:$TASK_STROKE,color:$NODE_TEXT;
    classDef github fill:$TARGET_FILL,stroke:$TARGET_STROKE,color:$NODE_TEXT;
    classDef gpu fill:$GPU_FILL,stroke:$GPU_STROKE,stroke-width:2px,color:$NODE_TEXT;
    class local_edit,local_push,local_submit,local_inspect,local_refine local;
    class github_repo github;
    class gpu_run gpu;
    style Local fill:$SUB_TASK_FILL,stroke:$SUB_TASK_STROKE
    style GitHub fill:$SUB_NEUTRAL_FILL,stroke:$SUB_NEUTRAL_STROKE
    style GPU fill:$SUB_GPU_FILL,stroke:$SUB_GPU_STROKE,stroke-width:3px
    style LocalLoop fill:$SUB_LOOP_FILL,stroke:$SUB_TASK_STROKE,stroke-width:2px
</code></pre>

<p>I may explore a <a href="https://modal.com">Modal</a> version of this set up soon to verify whether it is another good case for serverless computing.</p>

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:sundqvist" role="doc-endnote">
      <p>See Sundqvist, H., Berge, E., &amp; Kristjánsson, J. E. (1989). Condensation and cloud parameterization studies with a mesoscale numerical weather prediction model. Monthly Weather Review, 117(8), 1641-1657. <a href="#fnref:sundqvist" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:unet-claude" role="doc-endnote">
      <p>I suppose Claude suggested using a U-Net because that is what almost every submission did. <a href="#fnref:unet-claude" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:emacs-jupyter" role="doc-endnote">
      <p>I use <a href="https://github.com/martibosch/snakemacs">snakemacs</a>, a fully pixi-based emacs setup up that provides environment-aware Language Server Protocol (LSP) features and allows interactive execution of Jupyter notebooks as Python scripts using <a href="https://github.com/astoff/code-cells.el">code-cells</a>, <a href="https://github.com/emacs-jupyter/jupyter">emacs-jupyter</a> and <a href="https://github.com/renan-r-santos/pixi-kernel">pixi-kernel</a>. See also my post <a href="https://martibosch.github.io/jupyter-emacs-universe">“Jupyter in the Emacs universe”</a> on emacs-Jupyter integrations. <a href="#fnref:emacs-jupyter" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="python" /><category term="climate-informatics" /><category term="deep-learning" /><category term="torch" /><category term="snakemake" /><category term="pixi" /><category term="wandb" /><summary type="html"><![CDATA[I finally managed to “win” the Climate informatics 26 Hackathon on using deep learning to predict cloud cover. Too bad this happened only:]]></summary></entry><entry><title type="html">Jupyter in the Emacs universe</title><link href="https://martibosch.github.io/jupyter-emacs-universe/" rel="alternate" type="text/html" title="Jupyter in the Emacs universe" /><published>2023-05-24T00:00:00+00:00</published><updated>2023-05-24T00:00:00+00:00</updated><id>https://martibosch.github.io/jupyter-emacs-universe</id><content type="html" xml:base="https://martibosch.github.io/jupyter-emacs-universe/"><![CDATA[<h1 id="jupyter-in-the-emacs-universe">Jupyter in the Emacs universe</h1>

<blockquote>
  <p>2012: The IPython team released the <strong>IPython Notebook</strong>, and the world has never been the same</p>

  <p>– <cite>Jake Vanderplas</cite><sup id="fnref:notebook-jakevdp" role="doc-noteref"><a href="#fn:notebook-jakevdp" class="footnote" rel="footnote">1</a></sup></p>
</blockquote>

<p>Whether you use Jupyter notebooks or not, it seems hard to disagree with the quote above. I actually did not know that the name “Jupyter” is a reference to the three programming languages to which the IPython notebook was extended in 2014, i.e., Julia, Python and R - again, thank you Wikipedia. By 2018, more than 100 languages were already supported, and quoting Lorena Barba, “For data scientists, Jupyter has emerged as a de facto standard”<sup id="fnref:nature-jupyter" role="doc-noteref"><a href="#fn:nature-jupyter" class="footnote" rel="footnote">2</a></sup>. In 2021, Jupyter Notebooks voted by Nature readers as the third software codes that had biggest impact in their work (after the Fortran compiler and the Fast Fourier transform)<sup id="fnref:nature-ten-codes" role="doc-noteref"><a href="#fn:nature-ten-codes" class="footnote" rel="footnote">3</a></sup>.</p>

<p>Unlike Nature and Wikipedia, I would break down Jupyter notebooks into three components (instead of two): the user interface (UI) to edit code and text, the kernel that executes the code and the underlying JSON file format with the “.ipynb” extension into which notebooks are saved and shared. Altogether, the first two components are not much of a novelty since they essentially constitue a read–eval–print loop (REPL) environment, which were developed in the 1960s<sup id="fnref:lisp-repl" role="doc-noteref"><a href="#fn:lisp-repl" class="footnote" rel="footnote">4</a></sup>. In my view, the novelty is more about how the first two occur within a document-like notebook file editing experience which allows not only to write code cells, but also to move, execute and delete them as desired until the overall look of the notebook is considered satisfactory. At this point, one can save and export the notebook, obtaining a visually-appealing document mixing code, documentation and results, which collectively tells a story, aka, a <em>literate computational narrative</em><sup id="fnref:ten-rules" role="doc-noteref"><a href="#fn:ten-rules" class="footnote" rel="footnote">5</a></sup>.</p>

<p>However, this editing freedom comes at a non-negligible cost, i.e., the risk of exporting an inconsistent computational pipeline after moving and deleting cells that have been executed (potentially several times). As a matter of fact, in 2019, a study collected a corpus of ~1.16M notebooks from GitHub and found that only 3.17% could be executed providing the same results. Many experienced programmers have raised warnings about how notebooks obscure the state, which can be especially dangerous for beginners<sup id="fnref:notebooks-state" role="doc-noteref"><a href="#fn:notebooks-state" class="footnote" rel="footnote">6</a></sup>. Additionally, the JSON-based ipynb format of vanilla Jupyter notebooks poses many challenges when it comes to version control, practically requiring you to use external tools such as <a href="https://github.com/mwouts/jupytext">Jupytext</a> and <a href="https://github.com/jupyter/nbdime">nbdime</a> to prosper in the face of adversity. But I think that in any case, it is fair to say that, <em>if used properly</em>, Jupyter notebooks are a very powerful tool.</p>

<p>Let us now go back to the first of the three components of notebooks outlined above, namely the interface to edit notebooks. The Project Jupyter provides two options, both in the form of a web application: the classic Jupyter Notebook and JupyterLab, with the latter providing a more fully-featured integrated development environment (IDE) to edit notebooks and other text files, run consoles and manage files. Additionally, many other options have emerged in view of the popularity of Jupyter noteebooks, both web-based (e.g., Google Colab, Azure Machine Learning, GitHub Codespaces, DeepNote…) and client-based (e.g., VSCode, PyCharm, DataSpell…). Nevertheless, for Emacs users such as myself, these are hardly viable options, even when using extensions to emulate Emacs key bindings. Therefore, the goal of this post is to explore which Emacs configurations provide the best Jupyter-like experience.</p>

<h2 id="desired-features">Desired features</h2>

<p>Before we go any further, it is worth noting that this post is inevitably <em>highly opinionanted</em>, or in other words, influenced by the way in which I use Jupyter notebooks, which is twofold. First, like most users, I use notebook to interactively explore, e.g., a dataset, a library or the like. As the notebook code becomes more mature, I may manually move it to Python modules or scripts. Such a development process can lead to both Python libraries or data science repositories. This brings me to the second way in which I use notebooks, namely to <em>tell a story</em>, which can either be the documentation or user guide for a Python library as well as the computational pipeline to reproduce the results of an academic article.</p>

<p>Based on what has been said so far, the main desired features, in no particular order, are the following:</p>

<ul>
  <li><strong>Beyond (Python) code</strong>: mixing <strong>Python</strong> (or another programming another language), <strong>markdown</strong> and <strong>inline plots</strong> can constitute a great recipe to create <em>literate computational narratives</em>, so it is important to ensure that the Emacs setup supports them - otherwise there is practically no reason to use a hardly human-readable JSON-based format instead of plain text “.py” files.</li>
  <li><strong>Proper version control <em>with the possibility to include cell outputs</em></strong>: following the previous point, I am convinced that Jupyter notebooks would not have been this successful if web platforms such as GitHub or GitLab did not offer a rich rendering of notebook files, as reading them directly in the browser can be a very convenient way to navigate <em>computational narratives</em> such as tutorials, computational pipelines to reproduce an academic paper or the like. From this perspective, it is essential that the version-controlled notebooks <em>can</em> include the cell outputs - I emphasize the <em>can</em>, because in some use cases, it may be better to strip out the cell outputs to reduce file sizes and to ease version control, yet it is important that the option to include the outputs exists.</li>
  <li><strong>IDE features (environment-aware and notebook-wide)</strong>: as highlighted in the introduction, the out-of-order editing and execution of notebooks can dangerously obscure the state, which can be prone to errors and elusive results. Features provided by IDEs such as completion, documentation, on-the-fly syntax checking, navigation, refactoring and the like can help prevent many of these issues. In Emacs, there are several options to provide these IDE features<sup id="fnref:emacs-python-ide" role="doc-noteref"><a href="#fn:emacs-python-ide" class="footnote" rel="footnote">7</a></sup> - the two main challenges are to ensure that IDE features are aware of the Python environment used in each Emacs buffer, and notebook-wide, i.e., using information (such as imports and variables) defined not only in the cell being edited but in all the notebook cells.</li>
</ul>

<h2 id="overview-of-emacs-configurations-for-jupyter-notebooks">Overview of Emacs configurations for Jupyter notebooks</h2>

<p>The following sections assume an overall setup in which multiple virtual Python environments coexist in the same computer. To this end, my configuration<sup id="fnref:snakemacs" role="doc-noteref"><a href="#fn:snakemacs" class="footnote" rel="footnote">8</a></sup> uses conda (or more precisely, its <em>much faster</em> reimplementation named <a href="https://github.com/mamba-org/mamba">mamba</a>), with automatic detection and activation of the right conda environment for a buffer using <a href="https://github.com/necaris/conda.el">conda.el</a>. Then, Jupyter interacts with conda environments using the <a href="https://github.com/ipython/ipykernel">ipykernel</a> package<sup id="fnref:conda-ipykernel" role="doc-noteref"><a href="#fn:conda-ipykernel" class="footnote" rel="footnote">9</a></sup>.</p>

<p>Although I believe that conda features major advantages over other virtual environment and package managers<sup id="fnref:conda-advantages" role="doc-noteref"><a href="#fn:conda-advantages" class="footnote" rel="footnote">10</a></sup>, it should be possible to achieve an analogous setup using alternative tools (e.g., virtualenv and pip<sup id="fnref:virtualenv-jupyter" role="doc-noteref"><a href="#fn:virtualenv-jupyter" class="footnote" rel="footnote">11</a></sup>). Similarly, my configuration uses <a href="https://github.com/microsoft/pyright">pyright</a> (which actually <a href="https://anaconda.org/conda-forge/pyright">can be installed using conda</a>)<sup id="fnref:snakemacs-caveat" role="doc-noteref"><a href="#fn:snakemacs-caveat" class="footnote" rel="footnote">12</a></sup>, a language server protocol (LSP) to obtain IDE features, but feel free to use another tool (e.g., <a href="https://github.com/joaotavora/eglot">eglot</a>) if you prefer.</p>

<p>Let us now finally move on to experiencing Jupyter notebooks in Emacs.</p>

<h3 id="ein">EIN</h3>

<p>The first option is <a href="https://github.com/millejoh/emacs-ipython-notebook">Emacs IPython Notebook (EIN)</a>. Using it <a href="https://github.com/martibosch/snakemacs/tree/ein">requires very little Emacs configuration</a> and is quite straightforward: we need to launch a jupyter notebook server (e.g., by running <code class="language-plaintext highlighter-rouge">jupyter notebook</code> in a shell or running <code class="language-plaintext highlighter-rouge">M-x ein:run</code> in Emacs), then run <code class="language-plaintext highlighter-rouge">M-x ein:login</code>. Then, we can either open an existing notebook or select a kernel and create a new notebook. The key bindigns are very convenient and provide a very jupyter-like experience: <code class="language-plaintext highlighter-rouge">C-c C-a</code> and <code class="language-plaintext highlighter-rouge">C-c C-b</code> respectively create cells above and below the current cell, cells can be moved up and down using <code class="language-plaintext highlighter-rouge">M-&lt;up&gt;</code> and <code class="language-plaintext highlighter-rouge">M-&lt;down&gt;</code>, <code class="language-plaintext highlighter-rouge">C-c C-c</code> executes the current cell, <code class="language-plaintext highlighter-rouge">C-c C-w</code> copies the current cell and <code class="language-plaintext highlighter-rouge">C-c C-y</code> yanks it, and other bindings allow executing all cells, restarting the kernel and many more. Additionally, you can easily mix Python with markdown, IPython magics, inline plots and shell commands.</p>

<p><img src="/assets/images/jupyter_emacs/ein-bindings.gif" alt="Jupyter-like experience with EIN" /></p>

<p>Creating a new notebook for a specific Jupyter kernel is also quite straight-forward, i.e., the <code class="language-plaintext highlighter-rouge">ein:notebooklist</code> buffer lets you select an option among the existing kernels, and then you just need to click the <code class="language-plaintext highlighter-rouge">[New Notebook]</code> button.</p>

<p>While EIN provides an Emacs interface to emulate the web-based Jupyter notebook with its major advantages (i.e., cell organization, markdown rendering, inline results and plots…), such a resemblance comes at the cost of raising several important issues that require careful consideration. First, the “undo” command only works within a given cell, leading to unintuitive undo behaviors when moving accross cells<sup id="fnref:ein-undo" role="doc-noteref"><a href="#fn:ein-undo" class="footnote" rel="footnote">13</a></sup>. Furthermore, a critical aspect to consider is that there is no “undo” for cell deletion, which can easily prompt one to very strongly regret certain typing mistakes. Similarly, <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Auto-Save.html">Emacs auto-saving and file recovery features</a> do not apply to “ipynb” files edited with EIN, so you can easily lose valuable work if your computer crashes.</p>

<p>Finally, IDE features in EIN are provided using <a href="https://github.com/jorgenschaefer/elpy">elpy</a>, which provides notebook-wide completion, navigation and documentation, and can be environment aware by using <code class="language-plaintext highlighter-rouge">pyvenv-activate</code> or <code class="language-plaintext highlighter-rouge">pyvenv-workon</code>. However, based on my experience (and other users have reported similarly), the main issue with elpy is that <a href="https://elpy.readthedocs.io/en/latest/concepts.html#the-rpc-process">the background elpy process</a> quickly ends up consiming an entire CPU and provides very slow responses. See a minimal example of how completions and documentation easily become excessively slow:</p>

<p><img src="/assets/images/jupyter_emacs/ein-elpy.gif" alt="IDE features with EIN and elpy" /></p>

<p>Moreover, as of May 2023, elpy is unmantained, which I hope that prompts the EIN developers to switch to a LSP client or another package to provide IDE features.</p>

<h3 id="code-cells-and-emacs-jupyter">code-cells and emacs-jupyter</h3>

<p>Another notable option to make Emacs interact with Jupyter kernels is the <a href="https://github.com/nnicandro/emacs-jupyter">emacs-jupyter</a> package. The main difference with EIN is that emacs-jupyter does not provide a graphical UI to edit notebooks. Instead, it just provides the underlying interaction with Jupyter kernels within Emacs, thus giving the user more freedom to chose the notebook editing interface.</p>

<p>The most straight-forward way to use emacs-jupyter is to use the <code class="language-plaintext highlighter-rouge">M-x jupyter-run-repl</code> command to run a REPL using a specific Jupyter kernel, open a “.py” file (or whichever language extension corresponds to the Jupyter kernel), write Python code and send the desired lines, regions or the whole buffer to the REPL. While many developers may be perfectly happy with such an approach, it does not represent any major conceptual change with respect to the initial design of the REPL environments in the 1960s. Therefore, it is missing two of the main strengths of Jupyter notebooks, i.e., the organizational compartmentalization provided by notebook cells and the <em>storytelling</em> ability of the exported notebook file, mixing programming code, markdown and inline plots.</p>

<p>In order to emulate the cell-like organization of Jupyter notebooks, <a href="https://github.com/astoff/code-cells.el">code-cells</a> provides a lightweight mode to read, edit and write “.ipynb” files. To that end, it first converts the JSON-based “.ipynb” file to a plain-text script representation, in which certain Python comment syntax is interpreted as cell boundaries. By default, such a conversion is performed by Jupytext (which needs to be installed separately), but it can easily be configured to use any other command (e.g., pandoc) by customizing the <code class="language-plaintext highlighter-rouge">code-cells-convert-ipynb-style</code>. In fact, it is also possible to convert “.ipynb” files to another format such as markdown or <a href="https://orgmode.org">org</a>, and then edit them in the associated Emacs mode.</p>

<h4 id="notebooks-as-python-scripts">Notebooks as Python scripts</h4>

<p>When opening an existing notebook using the default settings, it is automatically converted to a Python script, with line comments of the form <code class="language-plaintext highlighter-rouge"># %%</code> defining cell boundaries. Additionally, the converted file begins with a YAML-like header block enclosed by two comment lines of the form <code class="language-plaintext highlighter-rouge"># ---</code>, which includes notebook metadata such as the kernel information as well as Jupytext format and version information. In order to execute cells, we can run <code class="language-plaintext highlighter-rouge">M-x jupyter-repl-associate-buffer</code> to associate the buffer to an existing emacs-jupyter REPL or create a new one choosing the appropriate Jupyter kernel. Cells with shell commands and IPython magics can be commented, which avoids syntax checking errors and allows the other IDE features to work properly<sup id="fnref:jupyter-lsp-completion" role="doc-noteref"><a href="#fn:jupyter-lsp-completion" class="footnote" rel="footnote">14</a></sup>. An associated caveat is that such cells have to be uncommented before they can be evaluated by emacs-jupyter - otherwise we are just evaluating Python comments and thus seeing no effect whatsoever<sup id="fnref:code-cells-magics-issue" role="doc-noteref"><a href="#fn:code-cells-magics-issue" class="footnote" rel="footnote">15</a></sup>. The following screencast illustrates how a notebook can be edited as Python script and its cells can be executed interactively using emacs-jupyter:</p>

<p><img src="/assets/images/jupyter_emacs/code-cells-py.gif" alt="Notebooks as Python scripts" /></p>

<p>The fact that we are editing a Python file-like buffer has several key advantages when compared to the web-like interface of EIN, i.e., IDE features work fast and other basic features such as undo, auto saving and recovery work as in any regular Emacs file. However, in my view, notebooks are displayed more nicely in EIN: there is no need for a YAML-like header block<sup id="fnref:jupytext-config-file" role="doc-noteref"><a href="#fn:jupytext-config-file" class="footnote" rel="footnote">16</a></sup>, cells feel more demarcated, markdown cells can be properly rendered instead of appearing as Python comments and most importantly, cell outputs (including plots) appear within the same notebook buffer.
The latter brings me to the main shortcoming of this approach: as of May 2023, it is not possible to include the cell outputs in script representations of Jupyter notebooks<sup id="fnref:jupytext-outputs" role="doc-noteref"><a href="#fn:jupytext-outputs" class="footnote" rel="footnote">17</a></sup>. As discussed in the introduction, this can be a crucial deficiency as it disables one of the major strengths of Jupyter notebooks, namely their <em>storytelling</em> ability. Without displaying cell outputs, notebooks are no longer such a suitable medium for tutorials, documentation or supporting materials of an academic article. Nonetheless, the combination of code-cells, Jupytext and emacs-jupyter can be very well suited to use cases where cell outputs can be ommited.</p>

<p>Finally, <a href="https://github.com/martibosch/snakemacs/tree/code-cells-py">the configuration provided for this section of the post</a> includes a couple of changes. First, I have found the default key bindings (all prefixed with <code class="language-plaintext highlighter-rouge">C-c %</code>) rather impractical, so the cell navigation, moving and execution key bindings are redefined to mimic EIN. Secondly, unlike EIN, code-cells and Jupytext do not provide any command to create a new notebook (with its metadata), so the configuration includes a function named <code class="language-plaintext highlighter-rouge">my/new-notebook</code>, which takes the new notebook file path and the desired Jupyter kernel as arguments.</p>

<h4 id="notebooks-as-org-files">Notebooks as org files</h4>

<p>As its name suggests, <a href="https://orgmode.org/">org mode</a> was initially created as an Emacs to organize notes and tasks. Nevertheless, what started lightweight markup language quickly evolved into a much more powerful system, notably thanks to <a href="https://orgmode.org/worg/org-contrib/babel/">Babel</a>, a feature of org mode<sup id="fnref:org-babel" role="doc-noteref"><a href="#fn:org-babel" class="footnote" rel="footnote">18</a></sup> that allows users to include and execute <a href="https://orgmode.org/manual/Working-with-Source-Code.html">source code blocks</a> within org files. This is actually quite reminiscent of Jupyter notebook - in fact, one may argue that org and Babel provide a considerable superset of features, as they allow using multiple programming languages in the same file and offer richer and more flexible export capabilities.</p>

<p>It is actually <a href="https://github.com/martibosch/snakemacs/tree/code-cells-org">very easy to configure code-cells to automatically convert notebooks to org files and edit them accordingly</a> - provided that pandoc is installed, <code class="language-plaintext highlighter-rouge">code-cells-convert-ipynb-style</code>:</p>

<div class="language-elisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">setq</span> <span class="nv">code-cells-convert-ipynb-style</span> <span class="o">'</span><span class="p">(</span>
	<span class="p">(</span><span class="s">"pandoc"</span> <span class="s">"--to"</span> <span class="s">"ipynb"</span> <span class="s">"--from"</span> <span class="s">"org"</span><span class="p">)</span>
	<span class="p">(</span><span class="s">"pandoc"</span> <span class="s">"--to"</span> <span class="s">"org"</span> <span class="s">"--from"</span> <span class="s">"ipynb"</span><span class="p">)</span>
	<span class="nv">org-mode</span><span class="p">))</span>
</code></pre></div></div>

<p>When opening the example notebook using the above configuration, the resulting org buffer converts Jupyter cells to <a href="https://orgmode.org/manual/Structure-of-Code-Blocks.html">org code blocks</a> with the <code class="language-plaintext highlighter-rouge">jupyter-python</code> language identifier, with the markdown cells which are translated to <a href="https://orgmode.org/worg/org-syntax.html">org syntax</a> (which start with a line of the form <code class="language-plaintext highlighter-rouge">&lt;&lt;a0c84e00&gt;&gt;</code> that corresponds to the Jupyter cell id). Like when editing notebooks as Python scripts, we need a running emacs-jupyter REPL to evaluate code blocks. However, to execute <code class="language-plaintext highlighter-rouge">jupyter-python</code> blocks in org mode, we also need <a href="https://orgmode.org/org.html#Using-sessions">a session</a>, which is started by the Jupyter kernel and maintains the state, e.g., including any variables or objects that have been defined so that they can be shared between code blocks. Sessions can be defined using the <code class="language-plaintext highlighter-rouge">session</code> <a href="https://orgmode.org/org.html#Using-Header-Arguments">header argument</a>, e.g.:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#+begin_src jupyter-python :session foo
</span><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="nn">rasterio</span> <span class="k">as</span> <span class="n">rio</span>
<span class="kn">from</span> <span class="nn">rasterio</span> <span class="kn">import</span> <span class="n">plot</span>
<span class="c1">#+end_src
</span></code></pre></div></div>

<p>By default, <code class="language-plaintext highlighter-rouge">emacs-jupyter</code> uses the kernel associated with the currently active environment (see a dedicated section about this in the introduction above), yet this can be overridden by setting the <code class="language-plaintext highlighter-rouge">:kernel</code> header argument. Additionally, it is most certainly desirable to set the <code class="language-plaintext highlighter-rouge">:async</code> header argument to <code class="language-plaintext highlighter-rouge">yes</code> to allow asynchronous execution of code-blocks - otherwise the buffer will be completely stalled until the code execution is completed. Altogether, an example code block looks as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#+begin_src jupyter-python :session foo :kernel jupyter-emacs :async yes
</span><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="nn">rasterio</span> <span class="k">as</span> <span class="n">rio</span>
<span class="kn">from</span> <span class="nn">rasterio</span> <span class="kn">import</span> <span class="n">plot</span>
<span class="c1">#+end_src
</span></code></pre></div></div>

<p>While this highlights another advantage of org over Jupyter, namely the ability to use multiple languages and sessions in a single file, having to set these header arguments in each code block is quite verbose when only a single session and language is required - which surely corresponds to most use cases. Luckily, such a repetition can easily be avoided by setting the <code class="language-plaintext highlighter-rouge">header-args</code> property at the beginning of the file<sup id="fnref:org-property-plus" role="doc-noteref"><a href="#fn:org-property-plus" class="footnote" rel="footnote">19</a></sup>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#+PROPERTY: header-args:jupyter-python :session foo :kernel jupyter-emacs
#+PROPERTY: header-args:jupyter-python+ :async yes
</code></pre></div></div>

<p>so that all <code class="language-plaintext highlighter-rouge">jupyter-python</code> src blocks include the defined header arguments implicitly. Note that although it is beyond the scope of this post, it is further possible to set the <code class="language-plaintext highlighter-rouge">session</code> property to use remote kernels and much more<sup id="fnref:replacing-jupyter-org" role="doc-noteref"><a href="#fn:replacing-jupyter-org" class="footnote" rel="footnote">20</a></sup>. In any case, once the session and the kernel have been properly configured, code blocks can be executed using the <code class="language-plaintext highlighter-rouge">C-c C-c</code> key bindings. The screencast below shows how the example notebook can be set up and executed as an org file buffer:</p>

<p><img src="/assets/images/jupyter_emacs/org-setup.gif" alt="Notebooks as org documents setup" /></p>

<p>Likewise notebooks as Python scripts, org files are plain text, so the basic Emacs undo, auto saving and recovery work out of the box. Additionally, code blocks feel more demarcated than comment-delimited cells in Python scripts, and results can be displayed inline, including images - to that end, run <code class="language-plaintext highlighter-rouge">org-toggle-inline-images</code> (by default bound to <code class="language-plaintext highlighter-rouge">C-c C-x C-v</code>) after a code block producing an image has been executed<sup id="fnref:org-inline-images" role="doc-noteref"><a href="#fn:org-inline-images" class="footnote" rel="footnote">21</a></sup>. Regarding key bindings, new code blocks (and other kinds of <a href="https://orgmode.org/org.html#Blocks-1">org mode blocks</a>) can be inserted using the <code class="language-plaintext highlighter-rouge">org-insert-structure-template</code> command, by default bound to <code class="language-plaintext highlighter-rouge">C-c C-,</code>, and <a href="https://orgmode.org/org.html#Key-bindings-and-Useful-Functions">Babel provides a series of useful functions bound to the <code class="language-plaintext highlighter-rouge">C-c C-v</code> prefix</a> that facilitate navigating to the next or previous code block (using <code class="language-plaintext highlighter-rouge">C-c C-v n</code> or <code class="language-plaintext highlighter-rouge">C-c C-v p</code> respectively) as well as to execute all code blocks in the buffer (bound to <code class="language-plaintext highlighter-rouge">C-c C-v b</code>). Even though I find these key bindings to be less handy than its counterparts in EIN and code-cells, this is understandable as org mode provides much more features and flexibility. Following the screencast above, the one below displays how code blocks can be edited and executed:</p>

<p><img src="/assets/images/jupyter_emacs/org-src-edit.gif" alt="Notebooks as org documents code editing" /></p>

<p>In my view, the main issue of using org to emulate Jupyter is related to how code blocks are edited. First, in order to get proper indentation and IDE features, code blocks must be edited in a separate buffer by using the <code class="language-plaintext highlighter-rouge">org-edit-special</code> command, by default bound to <code class="language-plaintext highlighter-rouge">C-c '</code>. But besides the inconvenience of having to use multiple buffers<sup id="fnref:org-inline-editing" role="doc-noteref"><a href="#fn:org-inline-editing" class="footnote" rel="footnote">22</a></sup> (especially in small screens), separate code editing buffers have a major drawback, i.e., they make getting proper IDE features excessively intricate (besides <a href="https://github.com/emacs-jupyter/jupyter#what-does-this-package-do">the default inspection and completion provided by emacs-jupyter</a>). In fact, even after asking around in Reddit and GitHub issues I have not managed to achieve it<sup id="fnref:org-ide-features" role="doc-noteref"><a href="#fn:org-ide-features" class="footnote" rel="footnote">23</a></sup>.</p>

<p>Finally, even though pandoc <a href="https://pandoc.org/releases.html#pandoc-2.19.2-2022-08-22">supports round-trip conversion from org to ipynb since version 2.19.2</a> (the org propertiy definitions at the start of the file are lost, but this is a minor detail), results from code blocks are ommited, hence the converted “.ipynb” files do not include cell outputs<sup id="fnref:org-ipynb-outputs" role="doc-noteref"><a href="#fn:org-ipynb-outputs" class="footnote" rel="footnote">24</a></sup>.</p>

<p>Overall, org is nothing but adaptable so if you are comfortable tweaking your Emacs configuration, it is likely possible to suit org to your needs. In my experience, my inability to obtain proper notebook-wide IDE features when editing code is too much of a drawback.</p>

<h4 id="a-final-note-on-polymode">A final note on polymode</h4>

<p>Before wrapping up, it is worth noting that many of the problems in getting proper edition of Python code in org files is because the Python major mode is not active by default in org buffers, which makes sense because code blocks can be in many languages. Nonetheless, there exist libraries to support multiple major modes in a single buffer in Emacs - a notable example is <a href="https://github.com/polymode/polymode">polymode</a>. It is possible to use code-cells with Jupytext or pandoc, transform notebooks to a plain-text representation such as markdown and then open them in a single polymode buffer so that fenced code blocks with a language identifier, e.g.:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>```python
print("Here is some Python code")
```
</code></pre></div></div>

<p>can be edited in the language-specific major mode and sent to a running REPL. Nevertheless, doing so requires non-trival configuration based on a thorough understanding of <a href="https://polymode.github.io/concepts/">how polymode works</a>. Like with org mode, I have searched through blogs, GitHub and Reddit and I not been able to find any working configuration to make polymode work properly with emacs-jupyter or LSP<sup id="fnref:polymode-jupyter" role="doc-noteref"><a href="#fn:polymode-jupyter" class="footnote" rel="footnote">25</a></sup>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>As one would expect, the choice of an appropriate setup largely depends on the use case. The key points to consider from the configurations reviewed in this post are summarized below.</p>

<h3 id="tldr">TL;DR</h3>

<p>Use <strong>EIN</strong> if you:</p>

<ul>
  <li>want an interface as close as possible to Jupyter notebook and Jupyterlab, with a <em>nice notebook display with code, rendered markdown and results</em></li>
  <li>want a <em>simple Emacs setup</em> that works out of the box but is inevitable <em>less customizable</em></li>
  <li>do not mind <em>slow IDE features</em> (e.g., completion, documentation…)</li>
  <li>are going to <em>be (very) careful</em> not to mistakenly delete cells and save your notebooks constantly</li>
</ul>

<p>Use <strong>notebooks as Python scripts</strong> (i.e., <strong>code-cells</strong> interacting with an <strong>emacs-jupyter</strong> kernel, with notebooks converted to Python scripts with <strong>Jupytext</strong>) if you:</p>

<ul>
  <li>want all the <em>advantages of working with plain text files</em> (i.e., fast IDE features, undo, autosave…)</li>
  <li>do not mind a <em>lightweight</em> emulation of the Jupyter notebook experience, with <em>outputs in separate buffers, lesser-demarcated cells and without rendered markdown</em></li>
</ul>

<p>Use <strong>notebooks as org files</strong> (i.e., <strong>org-mode</strong> interacting with one or serveral <strong>emacs-jupyter</strong> kernels, with notebooks converted to org files with <strong>pandoc</strong>) if you:</p>

<ul>
  <li>want a <em>highly versatile and custumizable</em> setup that largely <em>supersedes the features offered by Juptyer</em> (e.g., multiple kernels in the same file, checklists, agenda, calendar…), while keeping the <em>advantages of working with plain text files</em></li>
  <li>are comfortable with <em>hacky Emacs configurations</em> and do not mind a <em>steep learning curve</em> (e.g., lots of metadata involved such as properties or header arguments, complex key bindings…)</li>
</ul>

<h3 id="final-personal-thoughts">Final (personal) thoughts</h3>

<p>After much experimentation, my preference leans towards a setup where notebooks are by default edited as Python scripts (with code-cells, emacs-jupyter and jupytext as reviewed above), mainly because I find that the advantages of working with a plain-text file (i.e., fast IDE features, undo, autosave…) outweight the better notebook representation offered by EIN.</p>

<p>In my experience, the cell-centric feel provided by EIN and org-mode (as well as in Jupyter and Jupyterlab) <em>ease</em> the conceptual organization of code in a way that is not matched in code-cells, but this is merely a UI aspect that could be easily improved. On the other hand, I find the inability of the “notebooks as Python scripts” approach to include inline inputs<sup id="fnref:jupytext-outputs:1" role="doc-noteref"><a href="#fn:jupytext-outputs" class="footnote" rel="footnote">17</a></sup> to be a bit more troublesome, especially since it prevents including outputs in version control - again, hampering the <em>storytelling ability</em> of Jupyter notebooks. The latter is why my setup keeps the possibility of editing notebooks with EIN by running <code class="language-plaintext highlighter-rouge">M-x ein:run</code> or <code class="language-plaintext highlighter-rouge">M-x ein:login</code> and opening the notebooks from the EIN menu. I find this useful once the content of the notebook is solid and I just want to review it (e.g., follow its story, visualize outputs inline, proofread markdown…) before commiting it to version control. Actually, I often perform this stage in the browser so that I can see exactly what the notebook will look like in GitHub.</p>

<p>Finally, I have never needed to use more than one programming language or Jupyter kernel within the same file, therefore I have felt overwhelmed by org mode. But again, your needs may be different - in any case, I hope that this post provides a thorough and helpful overview of the strengths and weaknesses of each approach to write Jupyter notebooks within Emacs.</p>

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:notebook-jakevdp" role="doc-endnote">
      <p>See Jake VanderPlas (2015). “The State of the Stack”, In 14th Python in Science Conference (SciPy). <a href="https://speakerdeck.com/jakevdp/the-state-of-the-stack-scipy-2015-keynote">Slides</a>. <a href="#fnref:notebook-jakevdp" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nature-jupyter" role="doc-endnote">
      <p>See Jeffrey M. Perkel (2018). “Why Jupyter is data scientists’ computational notebook of choice”, Nature. <a href="https://www.nature.com/articles/d41586-018-07196-1">www.nature.com/articles/d41586-018-07196-1</a>. <a href="#fnref:nature-jupyter" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nature-ten-codes" role="doc-endnote">
      <p>See Jeffrey M. Perkel (2021). “Ten computer codes that transformed science”, Nature. <a href="https://www.nature.com/articles/d41586-021-00075-2">www.nature.com/articles/d41586-021-00075-2</a>. <a href="#fnref:nature-ten-codes" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:lisp-repl" role="doc-endnote">
      <p>See Deutsch, L., E. Berkeley, and D. G. Bobrow (1964). “The LISP implementation for the PDP-1 computer.”. <a href="http://s3data.computerhistory.org/pdp-1/DEC.pdp_1.1964.102650371.pdf">s3data.computerhistory.org/pdp-1/DEC.pdp_1.1964.102650371.pdf</a>. <a href="#fnref:lisp-repl" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ten-rules" role="doc-endnote">
      <p>See Rule, A., Birmingham, A., Zuniga, C., Altintas, I., Huang, S. C., Knight, R., … &amp; Rose, P. W. (2019). Ten simple rules for writing and sharing computational analyses in Jupyter Notebooks. PLoS computational biology, 15(7), e1007007. <a href="https://doi.org/10.1371/journal.pcbi.1007007">doi.org/10.1371/journal.pcbi.1007007</a>. <a href="#fnref:ten-rules" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:notebooks-state" role="doc-endnote">
      <p>I am thinking of the “I Don’t Like Notebooks” talk at JupyterCon 2018 by Joel Grus (<a href="https://docs.google.com/presentation/d/1n2RlMdmv1p25Xy5thJUhkKGvjtV-dkAIsUXP-AL4ffI]">link to the slides</a> but there have been many others. Overall, I would use <a href="https://www.reddit.com/r/Python/comments/9aoi35/comment/e4xlm8v/">a quote from Reddit about the talk by Joel Grus</a> to summarize the issue of notebooks, hidden state and reproducibility: “Notebooks are powerful, yes, but something can be said for mastering the screwdriver before diving into power tools.”. <a href="#fnref:notebooks-state" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:emacs-python-ide" role="doc-endnote">
      <p>See <a href="https://www.emacswiki.org/emacs/PythonProgrammingInEmacs">“Python Programming in Emacs”</a> at the EmacsWiki. <a href="#fnref:emacs-python-ide" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:snakemacs" role="doc-endnote">
      <p>The source code of my emacs setup is available at <a href="https://github.com/martibosch/snakemacs">martibosch/snakemacs</a>. <a href="#fnref:snakemacs" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:conda-ipykernel" role="doc-endnote">
      <p>To connect a given conda/mamba environment to Jupyter, you first need to activate the environment (i.e., run <code class="language-plaintext highlighter-rouge">conda activate &lt;env-name&gt;</code>), install ipykernel as in <code class="language-plaintext highlighter-rouge">conda install -c conda-forge ipykernel</code>, and finally, the environment’s Jupyter kernel can be registered to the list of available kernels by running <code class="language-plaintext highlighter-rouge">python -m ipykernel install --user --name=&lt;env-name&gt;</code>. <a href="#fnref:conda-ipykernel" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:conda-advantages" role="doc-endnote">
      <p>When it comes to managing virtual environments, I see two major advantages of conda/mamba, namely the ability of conda environments to manage the installation and update of Python itself (whereas virtualenv and venv rely on an existing and externally-managed Python executable - often the system Python itself) and manage non-Python packages, e.g., GDAL, CUDA dependencies and many more. See <a href="https://jakevdp.github.io/blog/2016/08/25/conda-myths-and-misconceptions">“Conda: Myths and Misconceptions” by Jake VanderPlas</a> and <a href="https://whiteboxml.com/blog/the-definitive-guide-to-python-virtual-environments-with-conda">“The definitive guide to Python virtual environments with conda” by WhiteBox</a>. <a href="#fnref:conda-advantages" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:virtualenv-jupyter" role="doc-endnote">
      <p>See <a href="https://towardsdatascience.com/create-virtual-environment-using-virtualenv-and-add-it-to-jupyter-notebook-6e1bf4e03415">“Create Virtual Environment using “virtualenv” and add it to Jupyter Notebook”</a> for an example alternative (to conda) setup for Python virtual environments and Jupyter using virtualenv. It should be possible to further enable automatic environment activation in Emacs using <a href="https://github.com/marcwebbie/auto-virtualenv">auto-virtualenv</a> or <a href="https://github.com/cybniv/poetry.el">poetry.el</a>. <a href="#fnref:virtualenv-jupyter" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:snakemacs-caveat" role="doc-endnote">
      <p>A caveat of the proposed conda and emacs setup is that pyright needs to be installed <em>within each conda environment</em> - otherwise the static type checking will not work when editing an Emacs buffer with an active conda environment. The same holds for formatting and linting tools such as <a href="https://github.com/psf/black">black</a> or <a href="https://beta.ruff.rs">ruff</a>. To ensure that the required packages are installed in all conda environments by default, you may use the <a href="https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html#config-add-default-pkgs"><code class="language-plaintext highlighter-rouge">create_default_packages</code> option</a> in your <code class="language-plaintext highlighter-rouge">.condarc</code> configuration file. See the <a href="https://github.com/martibosch/snakemacs#conda-environments-and-ide-features-for-python-buffers">“Conda environments and IDE features for Python buffers” section of the snakemacs <code class="language-plaintext highlighter-rouge">README.md</code></a> for a more detailed overview of the caveat. <a href="#fnref:snakemacs-caveat" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:ein-undo" role="doc-endnote">
      <p>See <a href="https://github.com/millejoh/emacs-ipython-notebook/issues/841">github.com/millejoh/emacs-ipython-notebook/issues/841</a>. <a href="#fnref:ein-undo" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jupyter-lsp-completion" role="doc-endnote">
      <p>The completion at point mechanisms from emacs-jupyter actually override those from LSP. Ideally, it should be possible to disable the emacs-jupyter completion in Python buffers so that all the IDE features are provided by LSP. See <a href="https://github.com/nnicandro/emacs-jupyter/issues/344">github.com/nnicandro/emacs-jupyter/issues/344</a>. <a href="#fnref:jupyter-lsp-completion" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:code-cells-magics-issue" role="doc-endnote">
      <p>In fact, as of May 2023 there is another related bug, which is that evaluating cells with <a href="https://ipython.readthedocs.io/en/stable/interactive/magics.html#cell-magics">cell-level magics</a> using the <code class="language-plaintext highlighter-rouge">code-cells-eval</code> command raises a Jupyter <code class="language-plaintext highlighter-rouge">UsageError</code>. Instead, cell-level magics work properly when selecting the cell region manually and using <code class="language-plaintext highlighter-rouge">jupyter-eval-region</code>. See <a href="https://github.com/astoff/code-cells.el/issues/18">github.com/astoff/code-cells.el/issues/18</a>. <a href="#fnref:code-cells-magics-issue" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jupytext-config-file" role="doc-endnote">
      <p>It should be noted that the header block can be reduced to only include the kernel information by setting the Jupytext metadata in a configuration file. See the <a href="https://jupytext.readthedocs.io/en/latest/config.html#jupytext-configuration-file">“Jupytext configuration file”</a> of the Jupytext documentation. <a href="#fnref:jupytext-config-file" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jupytext-outputs" role="doc-endnote">
      <p>There is actually a proposal by Marc Wouts (the author of jupytext) to support including outputs in <a href="https://jupytext.readthedocs.io/en/latest/formats.html#the-percent-format">the percent format</a> of Jupyter notebooks. <a href="#fnref:jupytext-outputs" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:jupytext-outputs:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:org-babel" role="doc-endnote">
      <p>Babel was initially an extension to org, but it is integrated into the org core since its version 7.0. See <a href="https://orgmode.org/org.html">the org Manual</a>. <a href="#fnref:org-babel" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:org-property-plus" role="doc-endnote">
      <p>In the provided snippet, the property has been defined in two lines <a href="https://orgmode.org/org.html#Property-Syntax">using the <code class="language-plaintext highlighter-rouge">+</code> sign</a> to avoid exceeding certain line length, but it could have been equivalently defined in a single line. <a href="#fnref:org-property-plus" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:replacing-jupyter-org" role="doc-endnote">
      <p>See the excellent post <a href="https://sqrtminusone.xyz/posts/2021-05-01-org-python">“Replacing Jupyter Notebook with Org Mode”</a>, by Pavel Korytov. <a href="#fnref:replacing-jupyter-org" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:org-inline-images" role="doc-endnote">
      <p>It is a bit hacky, but it is possible to configure org to display inline images automatically after execution of a respective code block by adding the following hook: <code class="language-plaintext highlighter-rouge">(add-hook 'org-babel-after-execute-hook 'org-display-inline-images 'append)</code>. See <a href="https://github.com/gregsexton/ob-ipython/blob/master/README.org#tips-and-tricks">the “Tips and tricks” section of the README file of <code class="language-plaintext highlighter-rouge">ob-ipython</code></a>. <a href="#fnref:org-inline-images" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:org-inline-editing" role="doc-endnote">
      <p>Furthermore, the tab and return keys are mapped to <code class="language-plaintext highlighter-rouge">org-cycle</code> and <code class="language-plaintext highlighter-rouge">org-return</code> respectively, which have quite a complex behaviour that is very tricky to configure. This can be disabled by setting <code class="language-plaintext highlighter-rouge">org-src-tab-acts-natively</code> to <code class="language-plaintext highlighter-rouge">nil</code>, but then the code is not automatically indented. There is quite a vast collection of threads in the <a href="https://www.reddit.com/r/emacs/">Emacs sub-reddit</a> and questions in the <a href="https://emacs.stackexchange.com">Emacs Stack Exchange</a> about this issue, which in my experience all discourage inline editing in org mode. <a href="#fnref:org-inline-editing" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:org-ide-features" role="doc-endnote">
      <p>While I have managed to obtain buffer-wide IDE features with <code class="language-plaintext highlighter-rouge">org-edit-special</code>, such a buffer corresponds to a single code block (equivalent to a single notebook cell). Therefore, the challenge is to obtain proper IDE features based on the whole org file (equivalent to the whole notebook), e.g., completion considering variables defined in other code blocks. In org mode, this would require <em>tangling</em> code blocks to a file and activating an LSP client. Nevertheless, this is <a href="https://github.com/emacs-lsp/lsp-mode/issues/2842">not yet officially documented</a> in lsp-mode, and the dedicated <a href="https://emacs-lsp.github.io/lsp-mode/manual-language-docs/lsp-org/">lsp-org</a> mode is still in an experimental/alpha phase. Similarly, this remains <a href="https://github.com/joaotavora/eglot/issues/523">an open issue in eglot</a>. Additionally, tangling should be automated and synchronized (e.g., with tools such as <a href="https://gitlab.com/mtekman/org-tanglesync.el">org-tanglesync</a> or <a href="https://github.com/yilkalargaw/org-auto-tangle">org-auto-tangle</a>) so that IDE features are updated as source blocks are edited. Finally, I have seen suggested <a href="https://orgmode.org/manual/Noweb-Reference-Syntax.html">the noweb syntax</a> may be used so that source code blocks can include references to other code blocks. However, as mentioned in the post above, I have not been able to find any working configuration that puts it all together to obtain org file-wide/notebook-wide IDE features. <a href="#fnref:org-ide-features" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:org-ipynb-outputs" role="doc-endnote">
      <p>The <a href="https://github.com/jkitchin/ox-ipynb">ox-ipynb</a> module allows exporting org files to ipynb while preserving cell outputs. The tool does not support round trip conversion, but it should be possible to use ox-ipynb to export to ipynb, and then pandoc to convert them back to org if the file needs to be edited again. However such a is not straightforward to configure using code-cells, as <code class="language-plaintext highlighter-rouge">code-cells-convert-ipynb-style</code> expects strings representing shell commands and ox-ipynb does not provide any - it provides Emacs commands only. <a href="#fnref:org-ipynb-outputs" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:polymode-jupyter" role="doc-endnote">
      <p>I have not managed to find any configuration that properly links emacs-jupyter with polymode. While it is possible to use emacs-jupyter commands such as <code class="language-plaintext highlighter-rouge">jupyter-eval-line-or-region</code> in Python code blocks of polymode buffers, by default these commands do not recognize the code block boundaries, and thus the code has to be evaluated line by line. This can possibly be achieved but doing so requires a comprehensive understanding of <a href="https://polymode.github.io/concepts/">how polymode works</a>, which at first glance I have found to be too demanding. On the other hand, regarding IDE features, I managed to make LSP work following <a href="https://github.com/polymode/polymode/issues/305#issuecomment-1018700437">an approach proposed in a GitHub issue</a>, but it was quite impractical in terms of performance as LSP was activated at each code block, therefore, to get environment-aware IDE features, this also requires activating the corresponding environment. Similarly to org mode, this allows you to use multiple languages in the same notebook-like file, yet I rarely - or practically never - need that. <a href="#fnref:polymode-jupyter" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="jupyter" /><category term="emacs" /><category term="git" /><summary type="html"><![CDATA[Jupyter in the Emacs universe]]></summary></entry><entry><title type="html">On spatial joins between networks and polygon geometries in Python</title><link href="https://martibosch.github.io/on-spatial-joins-networks-polygon-geometries/" rel="alternate" type="text/html" title="On spatial joins between networks and polygon geometries in Python" /><published>2020-05-19T00:00:00+00:00</published><updated>2020-05-19T00:00:00+00:00</updated><id>https://martibosch.github.io/on-spatial-joins-networks-polygon-geometries</id><content type="html" xml:base="https://martibosch.github.io/on-spatial-joins-networks-polygon-geometries/"><![CDATA[<p>The research of my colleague <a href="https://people.epfl.ch/armel.kemajou">Armel Kemajou</a> focuses on how informal transportation is shaping suburbanization in Sub-Saharan Africa.
To that end, he performed a field survey to collect a GPS traces of a sample of mototaxi drivers in the cities Yaoundé, Cameroon and Lomé, Togo (some results in Yaoundé have already been published as <a href="https://doi.org/10.1016/j.jtrangeo.2019.102472">a journal article</a>).
In order to address to what extent such emerging means of transportations pave the way for urban sprawl, it is crucial to explore the characteristics of the origin and destination locations of each trip.
Among other questions, we were therefore interested in the evaluating how developed is the street network around the destination locations.</p>

<p>After some preprocessing of the GPS traces in Yaoundé, we have a CSV file with the locations and times of the origins and destinations of 23712 trips during three weeks.
For the sake of simplicity, let us consider only the 8943 trips whose destination falls within the longitudes of 3.8° and 3.85° and the latitudes of 11.4° and 11.5°.
We then have a geo-data frame of the following form:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">trip_gdf</span><span class="p">.</span><span class="n">head</span><span class="p">()</span>
</code></pre></div></div>

<div>
<style scoped="">
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }

</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>origin_dt</th>
      <th>dest_dt</th>
      <th>neighborhood</th>
      <th>origin</th>
      <th>dest</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>415</th>
      <td>2018-02-07 09:01:12</td>
      <td>2018-02-07 09:45:09</td>
      <td>Nkozoa</td>
      <td>POINT (11.540371 3.965306)</td>
      <td>POINT (11.47033 3.83131)</td>
    </tr>
    <tr>
      <th>416</th>
      <td>2018-02-07 12:06:37</td>
      <td>2018-02-07 12:13:16</td>
      <td>Nkozoa</td>
      <td>POINT (11.471801 3.834358)</td>
      <td>POINT (11.48885 3.83186)</td>
    </tr>
    <tr>
      <th>422</th>
      <td>2018-02-07 13:52:37</td>
      <td>2018-02-07 14:15:07</td>
      <td>Nkozoa</td>
      <td>POINT (11.504751 3.867998)</td>
      <td>POINT (11.47140 3.82584)</td>
    </tr>
    <tr>
      <th>423</th>
      <td>2018-02-07 14:17:26</td>
      <td>2018-02-07 14:22:03</td>
      <td>Nkozoa</td>
      <td>POINT (11.471725 3.824733)</td>
      <td>POINT (11.45961 3.80135)</td>
    </tr>
    <tr>
      <th>424</th>
      <td>2018-02-07 15:08:45</td>
      <td>2018-02-07 15:22:56</td>
      <td>Nkozoa</td>
      <td>POINT (11.459691 3.80134)</td>
      <td>POINT (11.48888 3.83142)</td>
    </tr>
  </tbody>
</table>
</div>

<p>Let us then use <a href="https://github.com/gboeing/osmnx">OSMnx</a> to get the street network ot the area:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># get convex hull in lat/long to query OSM
</span><span class="n">convex_hull</span> <span class="o">=</span> <span class="n">trip_gdf</span><span class="p">[</span><span class="s">'dest'</span><span class="p">].</span><span class="n">unary_union</span><span class="p">.</span><span class="n">convex_hull</span>
<span class="c1"># get the street network and project it
</span><span class="n">G</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">graph_from_polygon</span><span class="p">(</span><span class="n">convex_hull</span><span class="p">,</span> <span class="n">network_type</span><span class="o">=</span><span class="s">'drive'</span><span class="p">)</span>
<span class="c1"># the undirected graph will do for our purposes
</span><span class="n">G</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">project_graph</span><span class="p">(</span><span class="n">G</span><span class="p">.</span><span class="n">to_undirected</span><span class="p">(</span><span class="n">reciprocal</span><span class="o">=</span><span class="bp">False</span><span class="p">))</span>
</code></pre></div></div>

<p>We obtain a network of 4032 nodes and 5344 edges which looks as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">plot_graph</span><span class="p">(</span><span class="n">G</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/network_spatial_joins/base-network.png" alt="png" /></p>

<p>Using the trips geo-data frame and the street network shown above, <strong>we want to compute, for each of the trip destinations, the total street length that falls within a 100 m radius of the destination location</strong>.
Let us use the code snippet below to illustrate the situation:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># let us first reproject `trip_gdf` to the network's CRS
</span><span class="n">dest_gdf</span> <span class="o">=</span> <span class="n">gpd</span><span class="p">.</span><span class="n">GeoDataFrame</span><span class="p">(</span><span class="n">geometry</span><span class="o">=</span><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'dest'</span><span class="p">])</span>
<span class="n">dest_gdf</span><span class="p">.</span><span class="n">crs</span> <span class="o">=</span> <span class="s">'epsg:4326'</span>
<span class="n">trip_gdf</span><span class="p">[</span><span class="s">'dest'</span><span class="p">]</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">project_gdf</span><span class="p">(</span><span class="n">loc_gdf</span><span class="p">)</span>

<span class="c1"># select the destination point of the first trip
</span><span class="n">point</span> <span class="o">=</span> <span class="n">trip_gdf</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">'dest'</span><span class="p">]</span>
<span class="n">point_buffer</span> <span class="o">=</span> <span class="n">point</span><span class="p">.</span><span class="nb">buffer</span><span class="p">(</span><span class="n">buffer_dist</span><span class="p">)</span>

<span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">plot_graph</span><span class="p">(</span><span class="n">G</span><span class="p">,</span> <span class="n">show</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span> <span class="n">close</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">point</span><span class="p">.</span><span class="n">y</span><span class="p">,</span> <span class="s">'x'</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'r'</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="o">*</span><span class="n">point_buffer</span><span class="p">.</span><span class="n">exterior</span><span class="p">.</span><span class="n">xy</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'g'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/network_spatial_joins/first-point-buffer.png" alt="png" /></p>

<p>Therefore, for each trip, we need to truncate the street network to the extent of the 100 m buffer around its destination location. To that end, we might use the <a href="https://osmnx.readthedocs.io/en/stable/osmnx.html?highlight=truncate#osmnx.core.truncate_graph_polygon"><code class="language-plaintext highlighter-rouge">truncate_graph_polygon</code></a> funcion of OSMnx as in the code snippet below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_buffer_street_length</span><span class="p">(</span><span class="n">point</span><span class="p">,</span> <span class="n">buffer_dist</span><span class="p">,</span> <span class="n">G</span><span class="p">):</span>
    <span class="n">geom</span> <span class="o">=</span> <span class="n">point</span><span class="p">.</span><span class="nb">buffer</span><span class="p">(</span><span class="n">buffer_dist</span><span class="p">)</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">buffer_G</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">truncate_graph_polygon</span><span class="p">(</span>
            <span class="n">G</span><span class="p">,</span> <span class="n">geom</span><span class="p">,</span> <span class="n">truncate_by_edge</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">retain_all</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="k">return</span> <span class="nb">sum</span><span class="p">([</span><span class="n">d</span><span class="p">[</span><span class="s">'length'</span><span class="p">]</span> <span class="k">for</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">buffer_G</span><span class="p">.</span><span class="n">edges</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">)])</span>
    <span class="k">except</span><span class="p">:</span>
        <span class="k">return</span> <span class="mi">0</span>
</code></pre></div></div>

<p>which we can then call by providing the <code class="language-plaintext highlighter-rouge">point</code> variable defined above, i.e., the location of the destination of the first trip, a buffer distance of 100 m, and the <em>projected</em> street network <code class="language-plaintext highlighter-rouge">G</code> as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">street_length</span> <span class="o">=</span> <span class="n">get_buffer_street_length</span><span class="p">(</span><span class="n">point</span><span class="p">,</span> <span class="mi">100</span><span class="p">,</span> <span class="n">G</span><span class="p">)</span>
</code></pre></div></div>

<p>Nevertheless, the such call takes 19.4 s in my Lenovo X1C6 laptop, which is clearly too slow considering that we need to perform such computation for each of the 8943 trips.</p>

<h2 id="first-step-spatial-join">First step: spatial join</h2>

<p>There are two main issues that we need to address regarding the performance of our code.</p>

<ul>
  <li><strong>Asymptotic complexity</strong>: for each of our K trips, we need to test which of the N nodes fall within the 100 m radius of the destination location. A brute force algorithm would require $\mathcal{O}(K \cdot N)$. In order to improve that, we can use a tree-based spatial indexing such as an <a href="https://en.wikipedia.org/wiki/R-tree">R-tree</a>. <a href="https://geoffboeing.com/2016/10/r-tree-spatial-index-python/">This post by Geoff Boeing</a> uses Python and GeoPandas to provide a nice overview of its advantages. Making use of such techniques can improve the asymptotic complexity to $\mathcal{O}((K + N) \cdot log(K) + f)$, where f is the number of interesections between minimum bounding rectangles found<sup id="fnref:spatial-join-techniques" role="doc-noteref"><a href="#fn:spatial-join-techniques" class="footnote" rel="footnote">1</a></sup>. Long story short, such gain becomes key when we deal with large datasets.</li>
  <li><strong>Dynamic typing and interpreted execution in Python</strong>: Python is an interpreted language which is prone to significant execution overheads because of its dynamic type-checking. In short, this means that <a href="https://nbviewer.jupyter.org/github/jakevdp/2013_fall_ASTR599/blob/master/notebooks/11_EfficientNumpy.ipynb">you should avoid iterating over large arrays and instead operate with NumPy typed arrays with <em>ufuncs</em>, aggregates (e.g., <code class="language-plaintext highlighter-rouge">np.mean</code>, <code class="language-plaintext highlighter-rouge">np.max</code>, …), slicing, masking and the like</a>.</li>
</ul>

<p>In Python, both issues can be addressed by making use of the spatial join operation of GeoPandas<sup id="fnref:geopandas" role="doc-noteref"><a href="#fn:geopandas" class="footnote" rel="footnote">2</a></sup>. To that end, we need to prepare two geo-data frames, i.e., a first one where the destination points of each trip are transformed into polygon geometries representing the circular buffers with a 100 m radius, and a second one with the point geometries of each node of the street network:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># left geo-data frame
# add a buffer around the point geometries
</span><span class="n">buffer_gdf</span> <span class="o">=</span> <span class="n">gpd</span><span class="p">.</span><span class="n">GeoDataFrame</span><span class="p">(</span>
    <span class="n">geometry</span><span class="o">=</span><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'dest'</span><span class="p">].</span><span class="nb">apply</span><span class="p">(</span><span class="k">lambda</span> <span class="n">p</span><span class="p">:</span> <span class="n">p</span><span class="p">.</span><span class="nb">buffer</span><span class="p">(</span><span class="n">buffer_dist</span><span class="p">)),</span>
    <span class="n">index</span><span class="o">=</span><span class="n">trip_gdf</span><span class="p">.</span><span class="n">index</span><span class="p">)</span>
<span class="n">buffer_gdf</span><span class="p">.</span><span class="n">crs</span> <span class="o">=</span> <span class="n">G</span><span class="p">.</span><span class="n">graph</span><span class="p">[</span><span class="s">'crs'</span><span class="p">]</span>

<span class="c1"># right geo-data frame
# get list of nodes from the projected graph
</span><span class="n">node_geom</span> <span class="o">=</span> <span class="p">[</span>
    <span class="n">geometry</span><span class="p">.</span><span class="n">Point</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s">'x'</span><span class="p">],</span> <span class="n">data</span><span class="p">[</span><span class="s">'y'</span><span class="p">])</span> <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">G</span><span class="p">.</span><span class="n">nodes</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="p">]</span>
<span class="c1"># create a GeoDataFrame with the node ids and their point geometries
</span><span class="n">node_gdf</span> <span class="o">=</span> <span class="n">gpd</span><span class="p">.</span><span class="n">GeoDataFrame</span><span class="p">(</span>
    <span class="p">{</span>
        <span class="s">'node'</span><span class="p">:</span> <span class="n">pd</span><span class="p">.</span><span class="n">Series</span><span class="p">(</span><span class="n">G</span><span class="p">.</span><span class="n">nodes</span><span class="p">()),</span>
        <span class="s">'geometry'</span><span class="p">:</span> <span class="n">node_geom</span>
    <span class="p">},</span> <span class="n">crs</span><span class="o">=</span><span class="n">G</span><span class="p">.</span><span class="n">graph</span><span class="p">[</span><span class="s">'crs'</span><span class="p">])</span>
</code></pre></div></div>

<p>Then we can perform the spatial join as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sjoin_gdf</span> <span class="o">=</span> <span class="n">gpd</span><span class="p">.</span><span class="n">sjoin</span><span class="p">(</span><span class="n">buffer_gdf</span><span class="p">,</span> <span class="n">node_gdf</span><span class="p">,</span> <span class="n">op</span><span class="o">=</span><span class="s">'contains'</span><span class="p">)</span>
</code></pre></div></div>

<p>which will test, whether each buffer geometry of <code class="language-plaintext highlighter-rouge">buffer_gdf</code> contains the nodes of the street network represented in <code class="language-plaintext highlighter-rouge">node_gdf</code>.
Since the resulting <code class="language-plaintext highlighter-rouge">sjoin_gdf</code> will have the same index as its first argument (i.e., the id of each buffer geometry, thus the id of each trip), we can use a group-by operation as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">trip_index</span><span class="p">,</span> <span class="n">group_gdf</span> <span class="ow">in</span> <span class="n">sjoin_gdf</span><span class="p">.</span><span class="n">groupby</span><span class="p">(</span><span class="n">sjoin_gdf</span><span class="p">.</span><span class="n">index</span><span class="p">):</span>
    <span class="k">break</span>  <span class="c1"># here we just want to see the first group
</span></code></pre></div></div>

<p>which will, for each buffer geometry, yield a geo-data frame of the form:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">group_gdf</span>
</code></pre></div></div>

<div>
<style scoped="">
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }

</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>geometry</th>
      <th>index_right</th>
      <th>node</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>415</th>
      <td>POLYGON ((774460.018 423876.374, 774459.536 42...</td>
      <td>619</td>
      <td>5795317038</td>
    </tr>
    <tr>
      <th>415</th>
      <td>POLYGON ((774460.018 423876.374, 774459.536 42...</td>
      <td>2962</td>
      <td>2322601884</td>
    </tr>
    <tr>
      <th>415</th>
      <td>POLYGON ((774460.018 423876.374, 774459.536 42...</td>
      <td>2970</td>
      <td>2322601912</td>
    </tr>
    <tr>
      <th>415</th>
      <td>POLYGON ((774460.018 423876.374, 774459.536 42...</td>
      <td>2974</td>
      <td>2322601917</td>
    </tr>
  </tbody>
</table>
</div>

<p>Note that the index and <code class="language-plaintext highlighter-rouge">geometry</code> column are the same since they correspond to the same buffer geometry (i.e., and in turn to the same trip), and therefore the <code class="language-plaintext highlighter-rouge">node</code> column shows us which nodes of <code class="language-plaintext highlighter-rouge">node_gdf</code> are inside such buffer geometry.</p>

<h2 id="compute-the-street-length-with-group-by-and-apply">Compute the street length with group by and apply</h2>

<p>Given the result of the spatial join, we can use the <a href="https://networkx.github.io/documentation/stable/reference/classes/generated/networkx.Graph.subgraph.html"><code class="language-plaintext highlighter-rouge">G.subgraph</code> method of NetworkX</a> to get the subgraph that corresponds to the set of nodes each <code class="language-plaintext highlighter-rouge">group_gdf</code>, i.e.:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">G</span><span class="p">.</span><span class="n">subgraph</span><span class="p">(</span><span class="n">group_gdf</span><span class="p">[</span><span class="s">'node'</span><span class="p">])</span>
</code></pre></div></div>

<p>In our case, in order to compute the street length of each buffer, we first need to define a function that will be called for each group data frame (such as the one shown above) of the group-by operation. Consider the following snippet:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># get street length
# first define function to compute street length given a list of nodes
# from G that fall within the buffer extent
</span><span class="k">def</span> <span class="nf">get_street_length</span><span class="p">(</span><span class="n">nodes</span><span class="p">,</span> <span class="n">G</span><span class="p">):</span>
    <span class="n">buffer_G</span> <span class="o">=</span> <span class="n">G</span><span class="p">.</span><span class="n">subgraph</span><span class="p">(</span><span class="n">nodes</span><span class="p">)</span>
    <span class="k">return</span> <span class="nb">sum</span><span class="p">([</span>
        <span class="n">d</span><span class="p">[</span><span class="s">'length'</span><span class="p">]</span> <span class="k">for</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">buffer_G</span><span class="p">.</span><span class="n">edges</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="p">])</span>
</code></pre></div></div>

<p>Then we can compute the street length of each trip (remember that the elements of <code class="language-plaintext highlighter-rouge">sjoin_gdf</code> are indexed by the trip ids) as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">]</span> <span class="o">=</span> <span class="n">sjoin_gdf</span><span class="p">[</span><span class="s">'node'</span><span class="p">].</span><span class="n">groupby</span><span class="p">(</span>
    <span class="n">sjoin_gdf</span><span class="p">.</span><span class="n">index</span><span class="p">).</span><span class="nb">apply</span><span class="p">(</span><span class="n">get_street_length</span><span class="p">,</span> <span class="n">G</span><span class="p">)</span>
</code></pre></div></div>

<p>There might be trips where their respective buffer geometry does not contain any graph node, which will be represented by a <code class="language-plaintext highlighter-rouge">nan</code> value (after the column assignment of the code snippet above). Since this means that no nodes of the street networks are found around the trip destination, we can fill such rows of <code class="language-plaintext highlighter-rouge">trip_gdf</code> with a 0.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">]</span> <span class="o">=</span> <span class="n">trip_gdf</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">].</span><span class="n">fillna</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<p>For the 8943 trips, the combination of (i) the spatial join, (ii) group-by computation of the street length and (iii) filling nan values takes a total of 2.66 s in my laptop.</p>

<p>We can now plot our distribution of street lengths as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_</span> <span class="o">=</span> <span class="n">trips_df</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">].</span><span class="n">hist</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/network_spatial_joins/street-length-subgraph.png" alt="png" /></p>

<h2 id="going-one-step-further-including-incomplete-street-segments">Going one step further: including incomplete street segments</h2>

<p>Although the computational workflow described above has provided us with a distribution of street lenghts in satisfactory time, it has a key conceptual shortcoming. Let us illustrate it by plotting the subgraph that correspond <code class="language-plaintext highlighter-rouge">group_gdf</code> used above:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fig</span><span class="p">,</span> <span class="n">ax</span> <span class="o">=</span> <span class="n">ox</span><span class="p">.</span><span class="n">plot_graph</span><span class="p">(</span><span class="n">G</span><span class="p">.</span><span class="n">subgraph</span><span class="p">(</span><span class="n">group_gdf</span><span class="p">[</span><span class="s">'node'</span><span class="p">]),</span>
                        <span class="n">margin</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
                        <span class="n">show</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
                        <span class="n">close</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="n">point</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">point</span><span class="p">.</span><span class="n">y</span><span class="p">,</span> <span class="s">'x'</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'r'</span><span class="p">)</span>
<span class="n">ax</span><span class="p">.</span><span class="n">plot</span><span class="p">(</span><span class="o">*</span><span class="n">point_buffer</span><span class="p">.</span><span class="n">exterior</span><span class="p">.</span><span class="n">xy</span><span class="p">,</span> <span class="n">color</span><span class="o">=</span><span class="s">'g'</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">show</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/network_spatial_joins/incomplete-graph.png" alt="png" /></p>

<p>As we can observe, the <code class="language-plaintext highlighter-rouge">subgraph</code> method returns a graph with only those nodes that are inside the buffer geometry and the edges between them. This might make sense for networks whose edges represent ethereal connections (e.g., social networks), but in networks whose edges represent physical connections (e.g., streets), we might be interested in retaining the edge segments that fall within the buffer geometry. To make such a point more clear, compare the subgraph plot above with the one below:</p>

<p><img src="/assets/images/network_spatial_joins/incomplete-segments.png" alt="png" /></p>

<p>The edges are colored in green when they link the green nodes, i.e., those that are inside the buffer geometry, like the graph returned by the <code class="language-plaintext highlighter-rouge">subgraph</code> method. The red nodes are nodes from the street network that are outside the buffer geometry but link to a node inside it (i.e., the red edges). The orange nodes, which are not originally in the street network, represent the intersection between the red edges and the boundaries of the buffer geometry. Finally, the orange edges (which overlay part of the red edges) represent the street segments that lie inside the buffer zones but lead to a node that is outside of it.</p>

<p>It seems clear that such orange segments must be considered in order to obtain a fair impression of the characteristics of the street network inside each buffer zone. It seems rather hasty to assume beforehand that ommiting them has no significant effect over the resulting distribution of street lenghts or that such residuals will be normally distributed.</p>

<p>To obtain the orange segments, we can use <a href="https://networkx.github.io/documentation/stable/reference/classes/generated/networkx.Graph.edges.html#networkx.Graph.edges">the <code class="language-plaintext highlighter-rouge">G.edges</code> method</a>, which takes as argument a list of nodes in <code class="language-plaintext highlighter-rouge">G</code>, and returns those edges that involve at least one of such nodes. We will then distinguish between two main cases: (a) edges such that both nodes are inside the buffer geometry (i.e., the green edges above) and (b) edges a node inside the buffer geometry and the other outisde it (i.e., the red edges above). In the latter case, we will use <a href="https://github.com/Toblerity/Shapely">Shapely</a> to get the interception between such edges and the boundaries of the buffer geometry (i.e., the orange nodes), and then add a synthetic edge between the inner node and the interception (i.e., the orange edges). Let us wrap the foregoing concepts into a function that can be called at each iteration of a group by loop as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_graph_in_geom</span><span class="p">(</span><span class="n">geom</span><span class="p">,</span> <span class="n">nodes</span><span class="p">,</span> <span class="n">G</span><span class="p">):</span>
    <span class="c1"># prepare the new graph
</span>    <span class="c1"># the `MultiGraph` type and the 'crs' and 'name' attributes are needed
</span>    <span class="c1"># to plot with osmnx
</span>    <span class="n">G_in_geom</span> <span class="o">=</span> <span class="n">nx</span><span class="p">.</span><span class="n">MultiGraph</span><span class="p">()</span>
    <span class="k">for</span> <span class="n">attr_key</span> <span class="ow">in</span> <span class="p">[</span><span class="s">'crs'</span><span class="p">,</span> <span class="s">'name'</span><span class="p">]:</span>
        <span class="n">G_in_geom</span><span class="p">.</span><span class="n">graph</span><span class="p">[</span><span class="n">attr_key</span><span class="p">]</span> <span class="o">=</span> <span class="n">G</span><span class="p">.</span><span class="n">graph</span><span class="p">[</span><span class="n">attr_key</span><span class="p">]</span>
    <span class="n">G_in_geom</span><span class="p">.</span><span class="n">add_nodes_from</span><span class="p">(</span><span class="n">G</span><span class="p">.</span><span class="n">subgraph</span><span class="p">(</span><span class="n">nodes</span><span class="p">).</span><span class="n">nodes</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>

    <span class="c1"># iterate over all the edges that involve at least one node of `nodes`
</span>    <span class="k">for</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">G</span><span class="p">.</span><span class="n">edges</span><span class="p">(</span><span class="n">nodes</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
        <span class="n">node_u</span> <span class="o">=</span> <span class="n">G</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">u</span><span class="p">]</span>
        <span class="n">node_v</span> <span class="o">=</span> <span class="n">G</span><span class="p">.</span><span class="n">nodes</span><span class="p">[</span><span class="n">v</span><span class="p">]</span>
        <span class="n">p_u</span> <span class="o">=</span> <span class="n">geometry</span><span class="p">.</span><span class="n">Point</span><span class="p">(</span><span class="n">node_u</span><span class="p">[</span><span class="s">'x'</span><span class="p">],</span> <span class="n">node_u</span><span class="p">[</span><span class="s">'y'</span><span class="p">])</span>
        <span class="n">p_v</span> <span class="o">=</span> <span class="n">geometry</span><span class="p">.</span><span class="n">Point</span><span class="p">(</span><span class="n">node_v</span><span class="p">[</span><span class="s">'x'</span><span class="p">],</span> <span class="n">node_v</span><span class="p">[</span><span class="s">'y'</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">p_u</span><span class="p">.</span><span class="n">within</span><span class="p">(</span><span class="n">geom</span><span class="p">):</span>
            <span class="k">if</span> <span class="n">p_v</span><span class="p">.</span><span class="n">within</span><span class="p">(</span><span class="n">geom</span><span class="p">):</span>
                <span class="c1"># both nodes are inside `geom`
</span>                <span class="n">G_in_geom</span><span class="p">.</span><span class="n">add_edge</span><span class="p">(</span><span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">length</span><span class="o">=</span><span class="n">d</span><span class="p">[</span><span class="s">'length'</span><span class="p">])</span>
                <span class="k">continue</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="c1"># `u` is inside `geom` but `v` is outside
</span>                <span class="n">node_in_id</span><span class="p">,</span> <span class="n">node_out_id</span> <span class="o">=</span> <span class="n">node_u</span><span class="p">[</span><span class="s">'osmid'</span><span class="p">],</span> <span class="n">node_v</span><span class="p">[</span><span class="s">'osmid'</span><span class="p">]</span>
                <span class="n">p_in</span><span class="p">,</span> <span class="n">p_out</span> <span class="o">=</span> <span class="n">p_u</span><span class="p">,</span> <span class="n">p_v</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="c1"># since the argument `nodes` (that we pass to `subgraph`
</span>            <span class="c1"># above) comes from the spatial join we know that at least one
</span>            <span class="c1"># node is inside `geom`
</span>            <span class="c1"># `v` is inside `geom` but `u` is outside
</span>            <span class="n">node_in_id</span><span class="p">,</span> <span class="n">node_out_id</span> <span class="o">=</span> <span class="n">node_v</span><span class="p">[</span><span class="s">'osmid'</span><span class="p">],</span> <span class="n">node_u</span><span class="p">[</span><span class="s">'osmid'</span><span class="p">]</span>
            <span class="n">p_in</span><span class="p">,</span> <span class="n">p_out</span> <span class="o">=</span> <span class="n">p_v</span><span class="p">,</span> <span class="n">p_u</span>

        <span class="c1"># if we get here, it is because we have `p_in` inside `geom` and
</span>        <span class="c1"># `p_out` outside, otherwise (i.e., both nodes inside `geom`) the
</span>        <span class="c1"># `continue` statement would have brought us directly to the next
</span>        <span class="c1"># iteration
</span>        <span class="n">intercept</span> <span class="o">=</span> <span class="n">geom</span><span class="p">.</span><span class="n">boundary</span><span class="p">.</span><span class="n">intersection</span><span class="p">(</span>
            <span class="n">geometry</span><span class="p">.</span><span class="n">LineString</span><span class="p">([</span><span class="n">p_in</span><span class="p">,</span> <span class="n">p_out</span><span class="p">]))</span>

        <span class="c1"># compute the distance between the node inside `geom` and the
</span>        <span class="c1"># interception between the edge and `geom`
</span>        <span class="n">dist</span> <span class="o">=</span> <span class="n">p_in</span><span class="p">.</span><span class="n">distance</span><span class="p">(</span><span class="n">intercept</span><span class="p">)</span>
        <span class="c1"># add the node for the interception with the same id as the node
</span>        <span class="c1"># that is outside of `geom`
</span>        <span class="n">G_in_geom</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="n">node_out_id</span><span class="p">,</span> <span class="n">x</span><span class="o">=</span><span class="n">intercept</span><span class="p">.</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="o">=</span><span class="n">intercept</span><span class="p">.</span><span class="n">y</span><span class="p">)</span>
        <span class="c1"># add an edge from the node inside `geom` to the interception
</span>        <span class="n">G_in_geom</span><span class="p">.</span><span class="n">add_edge</span><span class="p">(</span><span class="n">node_in_id</span><span class="p">,</span> <span class="n">node_out_id</span><span class="p">,</span> <span class="n">length</span><span class="o">=</span><span class="n">dist</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">G_in_geom</span>
</code></pre></div></div>

<p>Then we can finish our spatial join as in (note that using <a href="https://github.com/tqdm/tqdm">tqdm</a> to display a progress bar is optional):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">G_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">trip_index</span><span class="p">,</span> <span class="n">group_gdf</span> <span class="ow">in</span> <span class="n">notebook</span><span class="p">.</span><span class="n">tqdm</span><span class="p">(</span>
        <span class="n">sjoin_gdf</span><span class="p">.</span><span class="n">groupby</span><span class="p">(</span><span class="n">sjoin_gdf</span><span class="p">.</span><span class="n">index</span><span class="p">)):</span>
    <span class="n">buffer_geom</span> <span class="o">=</span> <span class="n">group_gdf</span><span class="p">.</span><span class="n">iloc</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="s">'geometry'</span><span class="p">]</span>
    <span class="n">nodes</span> <span class="o">=</span> <span class="n">group_gdf</span><span class="p">[</span><span class="s">'node'</span><span class="p">]</span>

    <span class="n">G_in_geom</span> <span class="o">=</span> <span class="n">get_graph_in_geom</span><span class="p">(</span><span class="n">buffer_geom</span><span class="p">,</span> <span class="n">nodes</span><span class="p">,</span> <span class="n">G</span><span class="p">)</span>
    <span class="n">G_list</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">G_in_geom</span><span class="p">)</span>
</code></pre></div></div>

<p>which will return a list of the graphs <code class="language-plaintext highlighter-rouge">G_list</code> where each position matches the index of <code class="language-plaintext highlighter-rouge">trip_gdf</code> features a NetworkX graph instance representing the street network inside the 100 m buffer around the destination of each trip. The execution of the above snippet takes 26.2 s in my laptop.</p>

<p>We can now compute the street length of each graph very easily with a function such as:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">compute_street_length</span><span class="p">(</span><span class="n">G</span><span class="p">):</span>
    <span class="k">return</span> <span class="nb">sum</span><span class="p">([</span><span class="n">d</span><span class="p">[</span><span class="s">'length'</span><span class="p">]</span> <span class="k">for</span> <span class="n">u</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">G</span><span class="p">.</span><span class="n">edges</span><span class="p">(</span><span class="n">data</span><span class="o">=</span><span class="bp">True</span><span class="p">)])</span>
</code></pre></div></div>

<p>which can be used in a loop as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">street_lengths</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">G_in_geom</span> <span class="ow">in</span> <span class="n">notebook</span><span class="p">.</span><span class="n">tqdm</span><span class="p">(</span><span class="n">G_list</span><span class="p">):</span>
    <span class="n">street_lengths</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">compute_street_length</span><span class="p">(</span><span class="n">G_in_geom</span><span class="p">))</span>
</code></pre></div></div>

<p>and takes 395 ms in my laptop. We can now plot the difference between the two approaches:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">figwidth</span><span class="p">,</span> <span class="n">figheight</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">rcParams</span><span class="p">[</span><span class="s">'figure.figsize'</span><span class="p">]</span>
<span class="n">fig</span><span class="p">,</span> <span class="p">(</span><span class="n">ax_l</span><span class="p">,</span> <span class="n">ax_r</span><span class="p">)</span> <span class="o">=</span> <span class="n">plt</span><span class="p">.</span><span class="n">subplots</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="n">figsize</span><span class="o">=</span><span class="p">(</span><span class="mi">2</span> <span class="o">*</span> <span class="n">figwidth</span><span class="p">,</span> <span class="n">figheight</span><span class="p">))</span>

<span class="c1"># left plot
</span><span class="n">alpha</span> <span class="o">=</span> <span class="mf">0.7</span>  <span class="c1"># to overlay histograms
</span><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">].</span><span class="n">hist</span><span class="p">(</span><span class="n">alpha</span><span class="o">=</span><span class="n">alpha</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">'inner only'</span><span class="p">,</span> <span class="n">ax</span><span class="o">=</span><span class="n">ax_l</span><span class="p">)</span>
<span class="n">street_length_ser</span><span class="p">.</span><span class="n">hist</span><span class="p">(</span><span class="n">alpha</span><span class="o">=</span><span class="n">alpha</span><span class="p">,</span> <span class="n">label</span><span class="o">=</span><span class="s">'intercept'</span><span class="p">,</span> <span class="n">ax</span><span class="o">=</span><span class="n">ax_l</span><span class="p">)</span>
<span class="n">ax_l</span><span class="p">.</span><span class="n">legend</span><span class="p">()</span>
<span class="n">ax_l</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="s">'Street lengths'</span><span class="p">)</span>

<span class="c1"># right plot
</span><span class="p">(</span><span class="n">trip_gdf</span><span class="p">[</span><span class="s">'street_length'</span><span class="p">]</span> <span class="o">-</span> <span class="n">street_length_ser</span><span class="p">).</span><span class="n">hist</span><span class="p">(</span><span class="n">ax</span><span class="o">=</span><span class="n">ax_r</span><span class="p">)</span>
<span class="n">ax_r</span><span class="p">.</span><span class="n">set_title</span><span class="p">(</span><span class="s">'Differences'</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/network_spatial_joins/difference-hists.png" alt="png" /></p>

<p>As noted in the left plot, the distribution of street lengths when considering the inner edges only and when including the street segments are quite different. From the plot in the right, we can see that such differences follow a normal distribution centered at -355.92 m.</p>

<h2 id="on-further-performance-improvements">On further performance improvements</h2>

<p>The 26 s required to include the incomplete street segments in our spatial join is significantly slower than the 2.66 seconds required when ommiting them. Nonetheless, there is still room for many performance improvements. Firstly, the group by for loop where we call <code class="language-plaintext highlighter-rouge">get_graph_in_geom</code> is <em>embarrassingly parallel</em>, and therefore <a href="https://examples.dask.org/applications/embarrassingly-parallel.html">can easily be performed at scale with Dask</a>. Secondly, <a href="https://graph-tool.skewed.de/performance">NetworkX is a rather slow library to model networks when compared to other Python alternatives such as graph-tool or igraph</a>. Thirdly, the geometric operations that we perform with shapely (i.e., interception of the red edges and the boundaries of the buffer geometries) are relatively simple and could be implemented with NumPy to avoid overheads. Finally, there exists a set of libraries that allow us to compile our Python code into faster machine languages and significantly improve the execution speeds <a href="https://flothesof.github.io/optimizing-python-code-numpy-cython-pythran-numba.html">e.g., Cython, Numba, Pythran…</a></p>

<p>I might at some point amend this blog post with the implementation of some of the ideas listed above. In the meantime, the proposed approach allows us to comfortably operate on networks of some thousand nodes in a laptop.</p>

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:spatial-join-techniques" role="doc-endnote">
      <p>See Jacox, E. H., &amp; Samet, H. (2007). Spatial join techniques. ACM Transactions on Database Systems (TODS), 32(1), 7-es. <a href="#fnref:spatial-join-techniques" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:geopandas" role="doc-endnote">
      <p>Actually, some geometry operations in GeoPandas used to be slow because the geometry elements were stored as a generic <code class="language-plaintext highlighter-rouge">object</code>-dtype column with Shapely objects. To improve that, an <a href="https://jorisvandenbossche.github.io/blog/2017/09/19/geopandas-cython/">initial idea by Joris Van den Bossche</a> consisted in changing the <code class="language-plaintext highlighter-rouge">object</code>-dtype gometry column for a NumPy array of pointers to GEOS objects. It seems that <a href="https://github.com/geopandas/geopandas/issues/473#issuecomment-610040595">such an approach has been put aside and the GeoPandas developers</a> in favor of <a href="https://geopandas.readthedocs.io/en/latest/install.html#using-the-optional-pygeos-dependency">making use of the spatial operations from the PyGEOS package</a>, although the latter is still experimental. In any case, the spatial join of GeoPandas employed in this article <a href="https://github.com/geopandas/geopandas/pull/475">was already Cythonized by Matthew Rocklin in August 2017</a>, so I do not know whether preformance improvements are to be expected in such operation. <a href="#fnref:geopandas" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="python" /><category term="networks" /><category term="geospatial" /><category term="openstreetmap" /><category term="geopandas" /><category term="networkx" /><category term="osmnx" /><summary type="html"><![CDATA[The research of my colleague Armel Kemajou focuses on how informal transportation is shaping suburbanization in Sub-Saharan Africa. To that end, he performed a field survey to collect a GPS traces of a sample of mototaxi drivers in the cities Yaoundé, Cameroon and Lomé, Togo (some results in Yaoundé have already been published as a journal article). In order to address to what extent such emerging means of transportations pave the way for urban sprawl, it is crucial to explore the characteristics of the origin and destination locations of each trip. Among other questions, we were therefore interested in the evaluating how developed is the street network around the destination locations.]]></summary></entry><entry><title type="html">Urban footprinter: a repeatable convolution-based approach to extract urban extents from land use/land cover rasters</title><link href="https://martibosch.github.io/urban-footprinter/" rel="alternate" type="text/html" title="Urban footprinter: a repeatable convolution-based approach to extract urban extents from land use/land cover rasters" /><published>2019-10-21T00:00:00+00:00</published><updated>2019-10-21T00:00:00+00:00</updated><id>https://martibosch.github.io/urban-footprinter</id><content type="html" xml:base="https://martibosch.github.io/urban-footprinter/"><![CDATA[<p>Administrative boundaries are often defined manually case-by-case and based on subjective judgement. As a consequence, their widespread use in the context of urban and enviornmental studies can lead to misleading outcomes [1, 2].</p>

<p>The aim of the urban footprinter package is to provide a repeatable convolution-based method to extract the extent of the urban regions.</p>

<p>The study area for this example is a rasterized extract of <a href="https://are.zh.ch/internet/baudirektion/are/de/geoinformation/Themen/amtliche_vermessung.html">the official cadastral survey of the Canton of Zurich, Switzerland</a>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">matplotlib.pyplot</span> <span class="k">as</span> <span class="n">plt</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">rasterio</span> <span class="k">as</span> <span class="n">rio</span>
<span class="kn">from</span> <span class="nn">matplotlib</span> <span class="kn">import</span> <span class="n">cm</span><span class="p">,</span> <span class="n">colors</span>
<span class="kn">from</span> <span class="nn">scipy</span> <span class="kn">import</span> <span class="n">ndimage</span> <span class="k">as</span> <span class="n">ndi</span>

<span class="kn">import</span> <span class="nn">urban_footprinter</span> <span class="k">as</span> <span class="n">ufp</span>


<span class="n">raster_filepath</span> <span class="o">=</span> <span class="s">'data/zurich.tif'</span>

<span class="k">with</span> <span class="n">rio</span><span class="p">.</span><span class="nb">open</span><span class="p">(</span><span class="n">raster_filepath</span><span class="p">)</span> <span class="k">as</span> <span class="n">src</span><span class="p">:</span>
    <span class="n">lulc_arr</span> <span class="o">=</span> <span class="n">src</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">res</span> <span class="o">=</span> <span class="n">src</span><span class="p">.</span><span class="n">res</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>  <span class="c1"># only square pixels are supported
</span>
    <span class="c1"># custom colormap to plot the raster
</span>    <span class="c1"># https://matplotlib.org/3.1.1/tutorials/colors/colormap-manipulation.html
</span>    <span class="n">num_classes</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">unique</span><span class="p">(</span><span class="n">lulc_arr</span><span class="p">))</span>
    <span class="n">terrain_r_cmap</span> <span class="o">=</span> <span class="n">cm</span><span class="p">.</span><span class="n">get_cmap</span><span class="p">(</span><span class="s">'terrain_r'</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">)</span>
    <span class="n">color_values</span> <span class="o">=</span> <span class="n">terrain_r_cmap</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">linspace</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">num_classes</span><span class="p">))</span>
    <span class="n">blue</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="mi">0</span><span class="p">,</span> <span class="p">.</span><span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">])</span>
    <span class="n">color_values</span><span class="p">[</span><span class="mi">13</span><span class="p">,</span> <span class="p">:]</span> <span class="o">=</span> <span class="n">blue</span>  <span class="c1"># water
</span>    <span class="n">color_values</span><span class="p">[</span><span class="mi">14</span><span class="p">,</span> <span class="p">:]</span> <span class="o">=</span> <span class="n">blue</span>  <span class="c1"># water
</span>    <span class="n">cmap</span> <span class="o">=</span> <span class="n">colors</span><span class="p">.</span><span class="n">ListedColormap</span><span class="p">(</span><span class="n">color_values</span><span class="p">)</span>

    <span class="c1"># np.where is used because nodata values of 255 would distort the colormap
</span>    <span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">lulc_arr</span> <span class="o">!=</span> <span class="mi">255</span><span class="p">,</span> <span class="n">lulc_arr</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">),</span> <span class="n">cmap</span><span class="o">=</span><span class="n">cmap</span><span class="p">)</span>
    <span class="n">plt</span><span class="p">.</span><span class="n">colorbar</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/zurich-lulc.png" alt="png" /></p>

<p>According to the land cover classification of the dataset, the codes 0 to 7 correspond to urban land cover classes. Therefore, the array of urban/non-urban pixels looks as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">urban_lulc_classes</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>
<span class="n">urban_lulc_arr</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">isin</span><span class="p">(</span><span class="n">lulc_arr</span><span class="p">,</span> <span class="n">urban_lulc_classes</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint32</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">urban_lulc_arr</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/zurich-urban-nonurban.png" alt="png" /></p>

<h2 id="how-the-urban-footprinter-works">How the urban footprinter works</h2>

<p>The approach of the urban footprinter library is built upon the methods used in the “Atlas of Urban Expansion” by Angel et al. [3]. The main idea is that a pixel is considered part of the urban extent depending on the proportion of built-up pixels that surround it. Accordingly, the extraction of the urban extent can be customized by means of the following three parameters:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">kernel_radius</code>: radius (in meters) of the circular kernel used in the convolution</li>
  <li><code class="language-plaintext highlighter-rouge">urban_threshold</code>: proportion of neighboring (within the kernel) urban pixels after which a given pixel is considered urban</li>
  <li><code class="language-plaintext highlighter-rouge">buffer_dist</code> (optional): buffer distance (in meters) to add around the computed mask</li>
</ul>

<h3 id="convolution-with-a-circular-kernel">Convolution with a circular kernel</h3>

<p>The main task consists in computing, for each pixel of the raster, the proportion of surrounding build-up pixels. This is done by means of a convolution with a circular kernel. In this example, the <code class="language-plaintext highlighter-rouge">kernel_radius</code> parameter will be set to 500 meters.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">kernel_radius</span> <span class="o">=</span> <span class="mi">500</span>

<span class="n">kernel_pixel_radius</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">kernel_radius</span> <span class="o">//</span> <span class="n">res</span><span class="p">)</span>
<span class="n">kernel_pixel_len</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">kernel_pixel_radius</span> <span class="o">+</span> <span class="mi">1</span>

<span class="n">y</span><span class="p">,</span> <span class="n">x</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">ogrid</span><span class="p">[</span><span class="o">-</span><span class="n">kernel_pixel_radius</span><span class="p">:</span><span class="n">kernel_pixel_len</span> <span class="o">-</span>
                <span class="n">kernel_pixel_radius</span><span class="p">,</span> <span class="o">-</span><span class="n">kernel_pixel_radius</span><span class="p">:</span>
                <span class="n">kernel_pixel_len</span> <span class="o">-</span> <span class="n">kernel_pixel_radius</span><span class="p">]</span>
<span class="n">mask</span> <span class="o">=</span> <span class="n">x</span> <span class="o">*</span> <span class="n">x</span> <span class="o">+</span> <span class="n">y</span> <span class="o">*</span> <span class="n">y</span> <span class="o">&lt;=</span> <span class="n">kernel_pixel_radius</span> <span class="o">*</span> <span class="n">kernel_pixel_radius</span>

<span class="n">kernel</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">zeros</span><span class="p">((</span><span class="n">kernel_pixel_len</span><span class="p">,</span> <span class="n">kernel_pixel_len</span><span class="p">),</span> <span class="n">dtype</span><span class="o">=</span><span class="n">np</span><span class="p">.</span><span class="n">uint32</span><span class="p">)</span>
<span class="n">kernel</span><span class="p">[</span><span class="n">mask</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
</code></pre></div></div>

<p>The kernel will be an array of ones (the circle) and zeros which looks as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">kernel</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">colorbar</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/convolution-kernel.png" alt="png" /></p>

<p>The convolution is computed by means of the <a href="https://docs.scipy.org/doc/scipy-0.16.0/reference/ndimage.html">convolve</a> function of SciPy.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">conv_result</span> <span class="o">=</span> <span class="n">ndi</span><span class="p">.</span><span class="n">convolve</span><span class="p">(</span><span class="n">urban_lulc_arr</span><span class="p">,</span> <span class="n">kernel</span><span class="p">)</span>
</code></pre></div></div>

<p>Since both <code class="language-plaintext highlighter-rouge">urban_lulc_arr</code> and <code class="language-plaintext highlighter-rouge">kernel</code> are arrays of ones and zeros, the result corresponds to the number of built-up pixels that lie within a 500m distance.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">conv_result</span><span class="p">)</span>
<span class="n">plt</span><span class="p">.</span><span class="n">colorbar</span><span class="p">()</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/convolution-result.png" alt="png" /></p>

<h3 id="filtering-the-convolution-results">Filtering the convolution results</h3>

<p>The result of the convolution is then used to classify pixels that are part of the urban extent. To this end, the <code class="language-plaintext highlighter-rouge">urban_threshold</code> parameter sets the proportion of surrounding built-up pixels after which a pixel is to be considered part of the urban extent. In this example, it will be set to 0.25 (i.e., 25%).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">urban_threshold</span> <span class="o">=</span> <span class="mf">0.25</span>

<span class="n">urban_mask</span> <span class="o">=</span> <span class="n">conv_result</span> <span class="o">&gt;=</span> <span class="n">urban_threshold</span> <span class="o">*</span> <span class="n">np</span><span class="p">.</span><span class="nb">sum</span><span class="p">(</span><span class="n">kernel</span><span class="p">)</span>
</code></pre></div></div>

<p>The result looks as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">urban_mask</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/convolution-threshold.png" alt="png" /></p>

<h3 id="optional-extract-only-the-largest-urban-cluster">Optional: extract only the largest urban cluster</h3>

<p>The fact that raster images are rectangular and real-world cities are not often entails that the raster includes more than just one urban settlement. In such case, the user might be interested in the extent that corresponds to the main urban settlement, namely, the largest urban patch of the raster. The largest urban cluster of the resulting <code class="language-plaintext highlighter-rouge">urban_mask</code> can be obtained by means of the <a href="https://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.ndimage.measurements.label.html#scipy.ndimage.measurements.label">label</a> function of SciPy, which performs a connected-component labeling of the input array (i.e., <code class="language-plaintext highlighter-rouge">urban_mask</code>).</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">kernel_moore</span> <span class="o">=</span> <span class="n">ndi</span><span class="p">.</span><span class="n">generate_binary_structure</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
<span class="n">label_arr</span> <span class="o">=</span> <span class="n">ndi</span><span class="p">.</span><span class="n">label</span><span class="p">(</span><span class="n">urban_mask</span><span class="p">,</span> <span class="n">kernel_moore</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">cluster_label</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">argmax</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">unique</span><span class="p">(</span><span class="n">label_arr</span><span class="p">,</span> <span class="n">return_counts</span><span class="o">=</span><span class="bp">True</span><span class="p">)[</span><span class="mi">1</span><span class="p">][</span><span class="mi">1</span><span class="p">:])</span> <span class="o">+</span> <span class="mi">1</span>
<span class="n">urban_mask</span> <span class="o">=</span> <span class="p">(</span><span class="n">label_arr</span> <span class="o">==</span> <span class="n">cluster_label</span><span class="p">).</span><span class="n">astype</span><span class="p">(</span><span class="n">np</span><span class="p">.</span><span class="n">uint8</span><span class="p">)</span>
</code></pre></div></div>

<p>which produces the following output:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">urban_mask</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/largest-urban-cluster.png" alt="png" /></p>

<h3 id="optional-adding-a-buffer-around-the-mask">Optional: adding a buffer around the mask</h3>

<p>In some contexts, e.g., the study of the urban-rural gradient in landscape ecology, it might be of interest to add a buffer around the urban extent. In this example, a buffer of 1000 meters will be added to the extracted urban extent. To this end, the <a href="https://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.ndimage.morphology.binary_dilation.html#scipy.ndimage.morphology.binary_dilation">binary_dilation</a> function of Scipy can be used as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">buffer_dist</span> <span class="o">=</span> <span class="mi">1000</span>

<span class="n">iterations</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">buffer_dist</span> <span class="o">//</span> <span class="n">res</span><span class="p">)</span>
<span class="n">kernel_moore</span> <span class="o">=</span> <span class="n">ndi</span><span class="p">.</span><span class="n">generate_binary_structure</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
<span class="n">urban_mask</span> <span class="o">=</span> <span class="n">ndi</span><span class="p">.</span><span class="n">binary_dilation</span><span class="p">(</span>
    <span class="n">urban_mask</span><span class="p">,</span> <span class="n">kernel_moore</span><span class="p">,</span> <span class="n">iterations</span><span class="o">=</span><span class="n">iterations</span><span class="p">)</span>
</code></pre></div></div>

<p>which produces the following output:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">urban_mask</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/urban-mask-buffered.png" alt="png" /></p>

<h2 id="the-urban-footprinter-api">The urban footprinter API</h2>

<p>In the urban footprinter package, the procedure described above is encapsulated into a single function named <code class="language-plaintext highlighter-rouge">urban_footprint_mask</code>, which accepts the following arguments:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>help(ufp.urban_footprint_mask)

Help on function urban_footprint_mask in module urban_footprinter:

urban_footprint_mask(raster, kernel_radius, urban_threshold, urban_classes=None, largest_patch_only=True, buffer_dist=None, res=None)
    Computes a boolean mask of the urban footprint of a given raster.

    Parameters
    ----------
    raster : ndarray or str, file object or pathlib.Path object
        Land use/land cover (LULC) raster. If passing a ndarray (instead of the
        path to a geotiff), the resolution (in meters) must be passed to the
        `res` keyword argument.
    kernel_radius : numeric
        The radius (in meters) of the circular kernel used in the convolution.
    urban_threshold : float from 0 to 1
        Proportion of neighboring (within the kernel) urban pixels after which
        a given pixel is considered urban.
    urban_classes : int or list-like, optional
        Code or codes of the LULC classes that must be considered urban. Not
        needed if `raster` is already a boolean array of urban/non-urban LULC
        classes.
    largest_patch_only : boolean, default True
        Whether the returned urban/non-urban mask should feature only the
        largest urban patch.
    buffer_dist : numeric, optional
        Distance to be buffered around the urban/non-urban mask. If no value is
        provided, no buffer is applied.
    res : numeric, optional
        Resolution of the `raster` (assumes square pixels). Ignored if `raster`
        is a path to a geotiff.

    Returns
    -------
    urban_mask : ndarray
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">kernel_radius</span> <span class="o">=</span> <span class="mi">500</span>
<span class="n">urban_threshold</span> <span class="o">=</span> <span class="mf">0.25</span>
<span class="n">urban_classes</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">8</span><span class="p">))</span>
</code></pre></div></div>

<p>Therefore, the urban extent extracted above can be obtained as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">urban_mask</span> <span class="o">=</span> <span class="n">ufp</span><span class="p">.</span><span class="n">urban_footprint_mask</span><span class="p">(</span><span class="n">raster_filepath</span><span class="p">,</span>
                                      <span class="n">kernel_radius</span><span class="p">,</span>
                                      <span class="n">urban_threshold</span><span class="p">,</span>
                                      <span class="n">urban_classes</span><span class="o">=</span><span class="n">urban_classes</span><span class="p">)</span>

<span class="n">plt</span><span class="p">.</span><span class="n">imshow</span><span class="p">(</span><span class="n">urban_mask</span><span class="p">)</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/urban-mask-ufp.png" alt="png" /></p>

<p>Alternatively, the urban extent can also be obtained as a vector geometry (instead of a raster array) by means of the <code class="language-plaintext highlighter-rouge">urban_footprint_mask_shp</code> function:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">urban_mask</span> <span class="o">=</span> <span class="n">ufp</span><span class="p">.</span><span class="n">urban_footprint_mask_shp</span><span class="p">(</span><span class="n">raster_filepath</span><span class="p">,</span>
                                          <span class="n">kernel_radius</span><span class="p">,</span>
                                          <span class="n">urban_threshold</span><span class="p">,</span>
                                          <span class="n">urban_classes</span><span class="o">=</span><span class="n">urban_classes</span><span class="p">)</span>

<span class="n">urban_mask</span>
</code></pre></div></div>

<p><img src="/assets/images/urban_footprinter/urban-mask-ufp.svg" alt="svg" /></p>

<h2 id="references">References</h2>

<ol>
  <li>Oliveira, E. A., Andrade Jr, J. S., &amp; Makse, H. A. (2014). Large cities are less green. Scientific reports, 4, 4235.</li>
  <li>Rozenfeld, H. D., Rybski, D., Andrade, J. S., Batty, M., Stanley, H. E., &amp; Makse, H. A. (2008). Laws of population growth. Proceedings of the National Academy of Sciences, 105(48), 18702-18707.</li>
  <li>Angel, S., Blei, A. M., Civco, D. L., &amp; Parent, J. (2016). Atlas of urban expansion - The 2016 edition, Volume 1: Areas and Densities. New York: New York University, Nairobi: UN-Habitat, and Cambridge, MA: Lincoln Institute of Land Policy.</li>
</ol>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="python" /><category term="urban-areas" /><category term="geospatial" /><category term="computer-vision" /><summary type="html"><![CDATA[Administrative boundaries are often defined manually case-by-case and based on subjective judgement. As a consequence, their widespread use in the context of urban and enviornmental studies can lead to misleading outcomes [1, 2].]]></summary></entry><entry><title type="html">Managing analysis workflows in geospatial data science with GNU Make</title><link href="https://martibosch.github.io/managing-analysis-workflows-geodata-make/" rel="alternate" type="text/html" title="Managing analysis workflows in geospatial data science with GNU Make" /><published>2019-02-25T00:00:00+00:00</published><updated>2019-02-25T00:00:00+00:00</updated><id>https://martibosch.github.io/managing-analysis-workflows-geodata-make</id><content type="html" xml:base="https://martibosch.github.io/managing-analysis-workflows-geodata-make/"><![CDATA[<p>When I started using Jupyter Notebooks, about three years ago, I was astonished of how well that suited my iterative approach to both data analysis and software development. Nevertheless, probably like many others, I also embraced some toxic habits<sup id="fnref:i-dont-like-notebooks" role="doc-noteref"><a href="#fn:i-dont-like-notebooks" class="footnote" rel="footnote">1</a></sup>.</p>

<p>The most troublesome of such practices was perhaps how I managed the data transformations required by my analysis workflow. For instance, after applying many complex transformation to a large pandas DataFrame, I would often conclude a notebook with a cell such as:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># dump the result
</span><span class="n">df</span><span class="p">.</span><span class="n">to_csv</span><span class="p">(</span><span class="s">'df_after_complex_transformation.csv'</span><span class="p">)</span>
</code></pre></div></div>

<p>so that the following notebook could immediately pick up from through an initial cell as in:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># resume from where last notebook left off
</span><span class="n">df</span> <span class="o">=</span> <span class="n">pd</span><span class="p">.</span><span class="n">read_csv</span><span class="p">(</span><span class="s">'df_after_complex_transformation.csv'</span><span class="p">)</span>
</code></pre></div></div>

<p>While many of such issues can be settled by choosing helpful file names, good organization, documentation and source control of the code, I felt that there was something missing in my coding toolbox. More precisely, I felt that for data-centric tasks, source control of the code was not enough. Imagine that I found out that I made a mistake in my complex transformation of the DataFrame above and fixed the code accordingly. Since data files like <code class="language-plaintext highlighter-rouge">df_after_complex_transformation.csv</code> are kept outside source control, when reopening the notebook after some days, perhaps even in another computer, it might be hard to know whether such interim data files are the ones that result from the older code with mistakes or, on the contrary, they are up to date with the fix.</p>

<p>It was not until some months ago, tired of sticking to toxic habits, that I started digging deep into blog posts and GitHub repositories in order to find a solution to this. The best solution came in the form of “A logical, reasonably standardized, but flexible project structure for doing and sharing data science work”, namely <a href="http://drivendata.github.io/cookiecutter-data-science/">Cookiecutter Data Science</a>.</p>

<p>As the reader may already know, there exist several tools devoted to manage such kind of computational workflows, and in fact, my initial idea was to write a post in which the appropriateness of such existing tools to geospatial data science was discussed. However, with the description of the example analysis case and the thorough explanation on how to use GNU Make to manage the corresponding computational workflow, the post has already become long enough. So a comparison with similar tools to manage geospatial computational workflows will be left for future posts. Now let us get to an example analysis case.</p>

<h2 id="an-example-from-geospatial-data-science-analysis-of-the-spatiotemporal-patterns-of-urbanization">An example from geospatial data science: analysis of the spatiotemporal patterns of urbanization</h2>

<p>I am currently working on a paper in which I intend to evaluate the spatiotemporal patterns of land cover change associated to urbanization in the main Swiss urban agglomerations. In order to do so, for each of the urban agglomerations I need to generate a time series of raster representations of the land cover. Two datasets are used for this purpose:</p>

<ul>
  <li>The <a href="https://land.copernicus.eu/pan-european/corine-land-cover">Corine Land Cover (CLC)</a> datasets for the years 2000, 2006 and 2012. Each dataset consists of a single <code class="language-plaintext highlighter-rouge">.tif</code> raster file for the whole European extent that features its land cover, categorized into 44 land cover classes.</li>
  <li>The Swiss <a href="https://www.bfs.admin.ch/bfs/de/home/dienstleistungen/geostat/geodaten-bundesstatistik/administrative-grenzen/generalisierte-gemeindegrenzen.assetdetail.5247306.html">Generalized Municipal Boundaries (GMB)</a>, which consist of a <code class="language-plaintext highlighter-rouge">.shp</code> vector shape file featuring the administrative boundaries of the municipalities that configure the main Swiss urban agglomerations. The boundaries for a given agglomeration can be extracted by filtering the boundary shapes by their attribute values in the <code class="language-plaintext highlighter-rouge">ANAME</code> column.</li>
</ul>

<p>The preprocessing of the data is as follows. For a given agglomeration, at a given year, we first clip the CLC raster of the corresponding year to the extent of the vector boundaries of the agglomeration in question. Since we only interested in the land cover change associated to urbanization, we will then reclassify the pixels of the clipped raster from the 44 land cover classes of CLC to a simplified urban/non-urban representation. The task is illustrated in the animation below.</p>

<p><img src="/assets/images/computational_workflows/crop-reclassify.gif" alt="Main task" /></p>

<p>We will then compute two landscape metrics: the <a href="http://www.umass.edu/landeco/research/fragstats/documents/Metrics/Area - Density - Edge Metrics/Metrics/C4 - PLAND.htm">proportion of landscape</a> occupied by urban land cover, and the <a href="http://www.umass.edu/landeco/research/fragstats/documents/Metrics/Shape Metrics/Metrics/P9 - FRAC.htm">area-weighted mean fractal dimension</a> of urban patches, which reflects the complexity of their shape.
Finally, we will assemble the computed values for each agglomeration and year in order to generate the following figure, which will help to assess whether the land use change associated to urbanization is leading towards a landscape with more complex shapes.</p>

<p><img src="/assets/images/computational_workflows/figure.png" alt="Figure" /></p>

<h2 id="enter-the-computational-workflow-approach">Enter the computational workflow approach</h2>

<p>Let us apply the <a href="http://drivendata.github.io/cookiecutter-data-science/">Cookiecutter Data Science</a> project structure and its principles to our example case. A repository following such structure, with all the materials needed to reproduce our analysis is available at <a href="https://github.com/martibosch/swiss-urbanization-post">GitHub</a>. Following the <a href="http://drivendata.github.io/cookiecutter-data-science/#analysis-is-a-dag">analysis as a Directed Acyclic Graph (DAG)</a> approach, a simplified version of the workflow for the task above can be represented as follows:</p>

<pre><code class="language-mermaid">graph LR
DOWNGMB(Download GMB) --&gt; GMB[GMB shp]
DOWNCLC(Download CLC) --&gt; CLC[CLC tif]
GMB -.-&gt; MAKEEXTRACT(Make extract)
CLC -.-&gt; MAKEEXTRACT
subgraph for each agglomeration and year
MAKEEXTRACT --&gt; URBANEXTRACT[Urban extract tif]
end
URBANEXTRACT -.-&gt; MAKEFIGURE(Make figure)
MAKEFIGURE --&gt; FIGURE[Figure png]
classDef task fill:$TASK_FILL,stroke:$TASK_STROKE,color:$NODE_TEXT;
classDef data fill:$DATA_FILL,stroke:$DATA_STROKE,color:$NODE_TEXT;
class DOWNGMB,DOWNCLC,MAKEEXTRACT,MAKEFIGURE task;
class GMB,CLC,URBANEXTRACT,FIGURE data;
</code></pre>

<p>The nodes with rounded corners represent tasks, whereas the ones with sharp corners represent data states (for instance, interim files). The dashed links towards the highlighted part denote that there should not be a single <em>make extract</em> and <em>urban extract</em> pair of nodes but a pair for each urban agglomeration and for each year. Since we consider the 3 largest Swiss urban agglomerations (Zurich, Geneva and Basel) for the four years 2000, 2006 and 2012, we will generate a total of 9 urban extracts, which will then be all used in order to produce the final figure.</p>

<p>Note that the computation of the two landscape metrics is done within the <em>make figure</em> task. Had it been a computationally-expensive task or had the computed metrics been required for any further step in our analysis, it might have been reasonable to add a dedicated step in the DAG, e.g., a <em>compute metrics</em> task between <em>urban extract</em> and <em>make figure</em>, perhaps dumping the computed metrics to an interim <em>csv</em> file.</p>

<h3 id="step-1-download-the-raw-datasets">Step 1: Download the raw datasets</h3>

<p>The <a href="http://drivendata.github.io/cookiecutter-data-science/#data-is-immutable">data immutability</a> principle basically states that data should never be edited manually, and that anyone should be able to fully reproduce our analysis using only the raw datasets and our code. Accordingly, the <em>download</em> steps download the raw datasets to a <code class="language-plaintext highlighter-rouge">data/raw</code> directory of our repository (whose files will be ignored by git).
While some datasets can be downloaded by anyone from open-data services like <a href="https://zenodo.org/">Zenodo</a>, other datasets like CLC require a registration. In the latter case, I store the raw datasets in a personal S3 bucket, and then create a Python script that downloads the files from the bucket using the <a href="https://github.com/boto/boto3">boto3</a> library.
Overall, such approach to the <em>download</em> steps ensure that the raw datasets will be the same regardless on whether I am running the analysis in my laptop or my office’s desktop computer.</p>

<h3 id="step-2-make-the-urban-extracts">Step 2: Make the urban extracts</h3>

<p>The <em>make extract</em> step perform the preprocessing task described above, taking datasets from <code class="language-plaintext highlighter-rouge">data/raw</code> as inputs and outputting the urban extracts raster files to <code class="language-plaintext highlighter-rouge">data/processed</code>.</p>

<h3 id="step-3-make-the-figure">Step 3: Make the figure</h3>

<p>Finally, the <em>make figure</em> will first use the <a href="https://github.com/martibosch/pylandstats">PyLandStats library</a> in order to compute the landscape metrics for the urban extract located at <code class="language-plaintext highlighter-rouge">data/processed</code>, plot their evolution for each agglomeration, and then save the figure to <code class="language-plaintext highlighter-rouge">reports/figures</code>.</p>

<h2 id="automating-the-workflow-with-gnu-make">Automating the workflow with GNU Make</h2>

<p>As noted above, we intend to automate the way we move the steps of our workflow, so that we do not need to ask whether we have applied or not certain data transformation nor whether such transformation has been performed according to the latest version of our code. Ideally, we want manage the execution of such steps explicitly (without hidden state) and in an automated manner, avoiding tricks such as caching an interim <code class="language-plaintext highlighter-rouge">df_after_complex_transformation.csv</code> file
To that end, we will use GNU Make.</p>

<p>Many people (like myself) always thought of Make as a tool to compile executables from source code, especially in C. Their syntax is quite simple. We create a file named <code class="language-plaintext highlighter-rouge">Makefile</code> at the root of our repository, and write a set of rules there.</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">target</span><span class="o">:</span> <span class="nf">prerequisites</span>
	commands
</code></pre></div></div>

<p>The way in which we can chain rules can be used to mimic our analysis DAG. A very straightforward way to do so would be with a <code class="language-plaintext highlighter-rouge">Makefile</code> such as:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">download_gmb</span><span class="o">:</span>
	python src/data/download_gmb.py

<span class="nl">download_clc</span><span class="o">:</span>
	python src/data/download_clc.py

<span class="nl">extracts</span><span class="o">:</span> <span class="nf">download_gmb download_clc</span>
	python src/data/make_urban_extracts.py

<span class="nl">figure</span><span class="o">:</span> <span class="nf">extracts</span>
	python src/visualization/make_figure.py
</code></pre></div></div>

<p>Then, from the repository’s root, we can run <code class="language-plaintext highlighter-rouge">make figure</code>, and Make will detect that the <code class="language-plaintext highlighter-rouge">figure</code> target depends on the <code class="language-plaintext highlighter-rouge">extracts</code> target, and that the latter depends on both <code class="language-plaintext highlighter-rouge">download_gmb</code> and <code class="language-plaintext highlighter-rouge">download_clc</code>, and therefore Make will automatically determine the right order in which the tasks must be executed in order to produce our figure. This is the idea beyond the concept of an “automated management of the computational workflow”.
Nevertheless, as elegant as the above <code class="language-plaintext highlighter-rouge">Makefile</code> might look, and despite the fact that it would succeed in producing our figure, we would be making a poor use of the features that Make has to offer.</p>

<h3 id="detecting-completed-targets">Detecting completed targets</h3>

<p>Imagine that our <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code> has a mistake which is (for example) labeling our figure’s axes incorrectly. If we fix such mistake, and run <code class="language-plaintext highlighter-rouge">make figure</code> again, Make will blindly execute again all of the necessary steps, including the download of 551.9 MB of unchanged raw datasets, and the generation of the exact same 9 urban extracts. Since our datasets could be of gigabytes, and our steps could be computationally expensive algorithms, we want our management of the computational workflow to automatically detect whether a task must be re-executed or not.</p>

<p>This idea is in fact central to Make. In order to exploit it, we just need that targets match file paths. For instance, we could refactor our download scripts to take the destination file path as command-line argument and download the files to such location. Under these circumstances, we could use such destination file path as target of the two download rules and prerequisite of the <code class="language-plaintext highlighter-rouge">extracts</code> rule, as in:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">data/raw/gmb/gd-b-00.03-875-gg18.zip</span><span class="o">:</span>
	python src/data/download_gmb.py data/raw/gmb/gd-b-00.03-875-gg18.zip
</code></pre></div></div>

<p>and we could further DRY our rule by storing such path in a variable of our <code class="language-plaintext highlighter-rouge">Makefile</code> as in:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GMB_ZIP_FILEPATH</span> <span class="o">=</span> data/raw/gmb/gd-b-00.03-875-gg18.zip

<span class="nl">$(GMB_ZIP_FILEPATH)</span><span class="o">:</span>
	python src/data/download_gmb.py <span class="nv">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>Note, however, that if we now want download the GMB dataset, the elegant command <code class="language-plaintext highlighter-rouge">make download_gmb</code> becomes the less appealing <code class="language-plaintext highlighter-rouge">make data/raw/gmb/gd-b-00.03-875-gg18.zip</code>. We might overcome this drawback by writing an additional dummy rule such as:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nl">download_gmb</span><span class="o">:</span> <span class="nf">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>so that now, whenever we call <code class="language-plaintext highlighter-rouge">make download_gmb</code>, Make will automatically detect whether the file path <code class="language-plaintext highlighter-rouge">data/raw/gmb/gd-b-00.03-875-gg18.zip</code> exists and accordingly decide whether the file should be downloaded (that is, running <code class="language-plaintext highlighter-rouge">src/data/download_gmb.py</code>) or not.</p>

<h4 id="two-comments-on-phony-targets">Two comments on Phony targets</h4>

<p>In Make, targets which do not correspond to any file path, such as <code class="language-plaintext highlighter-rouge">download_gmb</code>, are called <a href="https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html">Phony targets</a>. A typical example would be a rule which does not produce any output, for instance, <a href="https://github.com/martibosch/swiss-urbanization-post/blob/master/Makefile#L101">a rule to delete compiled Python files</a>. Two remarks are especially relevant in this situation:</p>

<ul>
  <li>
    <p>When a rule depends on a Phony target, Make will always execute the rule associated with the Phony target. This is in sharp contraposition to file path targets, which Make will ignore if the corresponding file is up to date. That means that dummy rules such as the proposed <code class="language-plaintext highlighter-rouge">download_gmb</code> cannot be used as rule prerequisites. That is, using</p>

    <div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># again, let us ignore `download_clc` for now
</span><span class="nl">extracts</span><span class="o">:</span> <span class="nf">download_gmb download_clc</span>
	python src/data/make_extracts.py
</code></pre></div>    </div>

    <p>instead of <code class="language-plaintext highlighter-rouge">extracts: $(GMB_ZIP_FILEPATH) download_clc</code>, implies that Make will no longer be able to detect whether downloading the GMB dataset is necessary or not. The bottom line is: you might add dummy rules with Phony targets to have a more elegant command-line interface (CLI) with your Makefile, but do not use such dummy rules as prerequisites of other rules unless you really want them to be executed regardless on the state of your workflow.</p>
  </li>
  <li>
    <p>Was there a file matching the path <code class="language-plaintext highlighter-rouge">download_gmb</code>, Make would assume that the target refers to such file. In order to avoid such conflicts, Phony targets can be explicitly declared as prerequisites of a rule with the <a href="https://www.gnu.org/software/make/manual/html_node/Special-Targets.html#Special-Targets">special target</a> <code class="language-plaintext highlighter-rouge">.PHONY</code> as in:</p>

    <div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">download_gmb</span>

<span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nl">download_gmb</span><span class="o">:</span> <span class="nf">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div>    </div>
  </li>
</ul>

<h4 id="file-modification-times-and-order-only-prerequisites">File modification times and order-only prerequisites</h4>

<p>Some rules might require creating directories in our tree. For instance, the <code class="language-plaintext highlighter-rouge">data/raw/gmb</code> directory in which <code class="language-plaintext highlighter-rouge">gd-b-00.03-875-gg18.zip</code> is to be downloaded might not exist. There are many ways of dealing with such situation, for instance, within the Python script. Nonetheless, <em>handling this explicitly within the Makefile might be preferable, in order to avoid potential sources of hidden state</em>. We might thus add an additional rule that creates the <code class="language-plaintext highlighter-rouge">data/raw/gmb</code> directory only when such path does not exist (otherwise it would throw a <code class="language-plaintext highlighter-rouge">File exists</code> error). We can do so as follows:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GMB_DIR</span> <span class="o">=</span> data/raw/gmb
<span class="c"># Since `$(GMB_DIR)` is constant, we use `:=` so that Make expands the path
# only once instead of doing so every time `GMB_ZIP_FILEPATH` is used.
# See https://www.gnu.org/software/make/manual/make.html#Flavors
</span><span class="nv">GMB_ZIP_FILEPATH</span> <span class="o">:=</span> <span class="nv">$(GMB_DIR)</span>/gd-b-00.03-875-gg18.zip

<span class="nl">$(GMB_DIR)</span><span class="o">:</span>
	<span class="nb">mkdir</span> <span class="nv">$(GMB_DIR)</span>

<span class="nl">$(GMB_ZIP_FILEPATH)</span><span class="o">:</span> <span class="nf">$(GMB_DIR)</span>
	python src/data/download_gmb.py <span class="nv">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>Let us go further. Since our tasks will require the use of the GMB shapefiles rather than of the GMB zip, we might use the <code class="language-plaintext highlighter-rouge">unzip</code> command in order to extract them. We can even remove the GMB zip once the shapefiles are extracted. This could all be done as in:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">GMB_DIR</span> <span class="o">=</span> data/raw/gmb
<span class="nv">GMB_ZIP_FILEPATH</span> <span class="o">:=</span> <span class="nv">$(GMB_DIR)</span>/gd-b-00.03-875-gg18.zip
<span class="nv">GMB_SHP_FILEPATH</span> <span class="o">:=</span> <span class="nv">$(GMB_DIR)</span>/g1a18.shp

<span class="nl">$(GMB_DIR)</span><span class="o">:</span>
	<span class="nb">mkdir</span> <span class="nv">$(GMB_DIR)</span>

<span class="nl">$(GMB_SHP_FILEPATH)</span><span class="o">:</span> <span class="nf">$(GMB_DIR)</span>
	python src/data/download_gmb.py <span class="nv">$(GMB_ZIP_FILEPATH)</span>
	unzip <span class="nv">$(GMB_ZIP_FILEPATH)</span> <span class="nt">-d</span> <span class="nv">$(GMB_DIR)</span>
	<span class="nb">rm</span> <span class="nv">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>A peculiar issue can arise with the above Makefile. When using <code class="language-plaintext highlighter-rouge">unzip</code> command, the modification times of the extracted files (such as <code class="language-plaintext highlighter-rouge">$(GMB_SHP_FILEPATH)</code>) will be those of their creation, whereas for <code class="language-plaintext highlighter-rouge">$(GMB_DIR)</code>, the modification time will correspond to the moment in which the files were extracted under such directory. Accordingly, in the eyes of Make, the target <code class="language-plaintext highlighter-rouge">$(GMB_SHP_FILEPATH)</code> will appear older than its prerequisite <code class="language-plaintext highlighter-rouge">$(GMB_DIR)</code>, and therefore the rule will always be executed.</p>

<p>There are two ways to work around such issue. On the one hand, we could use the <code class="language-plaintext highlighter-rouge">touch</code> command, which will set the modification date of <code class="language-plaintext highlighter-rouge">$(GMB_SHP_FILEPATH)$</code> to the current date<sup id="fnref:unzip-touch" role="doc-noteref"><a href="#fn:unzip-touch" class="footnote" rel="footnote">2</a></sup>, as in:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nl">$(GMB_SHP_FILEPATH)</span><span class="o">:</span> <span class="nf">$(GMB_DIR)</span>
	python src/data/download_gmb.py <span class="nv">$(GMB_ZIP_FILEPATH)</span>
	unzip <span class="nv">$(GMB_ZIP_FILEPATH)</span> <span class="nt">-d</span> <span class="nv">$(GMB_DIR)</span>
	<span class="nb">touch</span> <span class="nv">$(GMB_SHP_FILEPATH)</span>
	<span class="nb">rm</span> <span class="nv">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>Alternatively, we could also tell Make that <code class="language-plaintext highlighter-rouge">$(GMB_DIR)</code> is an <a href="https://www.gnu.org/software/make/manual/html_node/Prerequisite-Types.html">order-only prerequisites</a>, which ignore the modification dates and are executed only if the files or directories in question do not exist. Order-only prerequisites must be placed to the right of a pipe symbol <code class="language-plaintext highlighter-rouge">|</code>, which in our example would be:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="c"># a target might have both normal and order-only prerequisites, e.g.,
# $(GMB_SHP_FILEPATH): some-normal-prerequisite-target | $(GMB_DIR)
</span><span class="nl">$(GMB_SHP_FILEPATH)</span><span class="o">:</span> <span class="nf">| $(GMB_DIR)</span>
	python src/data/download_gmb.py <span class="nv">$(GMB_ZIP_FILEPATH)</span>
	unzip <span class="nt">-j</span> <span class="nv">$(GMB_ZIP_FILEPATH)</span> <span class="nt">-d</span> <span class="nv">$(GMB_DIR)</span>
	<span class="nb">rm</span> <span class="nv">$(GMB_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>Again, note that we could use <a href="https://docs.python.org/3/library/zipfile.html">Python’s zipfile library</a> and deal with the extraction of the shapefiles within the script <code class="language-plaintext highlighter-rouge">src/data/download_gmb.py</code>, nevertheless this will further complicate the non-trivial task of ensuring that Make manages our computational workflow correctly.</p>

<h3 id="pattern-rules-and-abstracting-our-workflow-from-the-data">Pattern rules and abstracting our workflow from the data</h3>

<p>Let us now consider the task of downloading the CLC datasets. Like GMB, CLC datasets come as a zip file, from which we intend to extract a geotiff file (instead of shapefiles in GMB). The major difference with respect to GMB is that we now need to download three datasets, namely one for each year. Accordingly, <code class="language-plaintext highlighter-rouge">src/data/download_clc.py</code> needs to know which CLC dataset to download in every case. Since CLC names its datasets as in <code class="language-plaintext highlighter-rouge">g100_clc00_V18_5</code>, <code class="language-plaintext highlighter-rouge">g100_clc06_V18_5</code> and <code class="language-plaintext highlighter-rouge">g100_clc12_V18_5</code> (note the year codes <code class="language-plaintext highlighter-rouge">00</code>, <code class="language-plaintext highlighter-rouge">06</code> and <code class="language-plaintext highlighter-rouge">12</code> right after the <code class="language-plaintext highlighter-rouge">clc</code> characters), let <code class="language-plaintext highlighter-rouge">src/data/download_clc.py</code> take such basename as first argument (to know which dataset to download), and as usual, the destination file path as second argument. Consider the following snippet:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Note that we should first handle the creation of the `data/raw/clc`
# directory like explained above for `$(GMB_DIR)`
</span><span class="nv">CLC_DIR</span> <span class="o">=</span> data/raw/clc

<span class="nl">$(CLC_DIR)</span><span class="o">:</span>
	<span class="nb">mkdir</span> <span class="nv">$(CLC_DIR)</span>

<span class="c"># and now we can start with the CLC dataset for the year code 00 (2000)
</span><span class="nv">CLC_00_BASENAME</span> <span class="o">=</span> g100_clc00_V18_5
<span class="nv">CLC_00_ZIP_FILEPATH</span> <span class="o">:=</span> data/raw/clc/<span class="nv">$(CLC_00_BASENAME)</span>.zip

<span class="nl">$(CLC_00_ZIP_FILEPATH)</span><span class="o">:</span> <span class="nf">| $(CLC_DIR)</span>
	python src/data/download_clc.py <span class="nv">$(CLC_00_BASENAME)</span> <span class="se">\</span>
	    <span class="nv">$(CLC_00_ZIP_FILEPATH)</span>
</code></pre></div></div>

<p>Since writing the foregoing rule for each dataset year would clearly be over-repetitive and become hard to maintain, Make features <a href="https://www.gnu.org/software/make/manual/html_node/Pattern-Rules.html">pattern rules</a>, which are like ordinary rules but use a pattern-matching mechanism on file paths. We can tell make that the commands to any zip target within <code class="language-plaintext highlighter-rouge">$(CLC_DIR)</code> are as follows:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nl">$(CLC_DIR)/%.zip</span><span class="o">:</span> <span class="nf">| $(CLC_DIR)</span>
	python src/data/download_clc.py <span class="nf">$(</span><span class="nb">notdir</span> <span class="nv">$@</span><span class="nf">)</span> <span class="nv">$@</span>
</code></pre></div></div>

<p>where the <code class="language-plaintext highlighter-rouge">%</code> is used to compose the target pattern. We could also tell Make, for example, to only match zip files starting by <code class="language-plaintext highlighter-rouge">g100_clc</code> as in <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/g100_clc%.zip</code>. On the other hand, the full expanded path of the target (in the above example, that is <code class="language-plaintext highlighter-rouge">data/raw/clc/g100_clc00_V18_5.zip</code>) will be stored under the <code class="language-plaintext highlighter-rouge">$@</code> <a href="https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#Automatic-Variables">automatic variable</a>, which we can use within the rule commands. Given how we have assumed <code class="language-plaintext highlighter-rouge">src/data/download_clc.py</code> to work, we can use <a href="https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html">Make’s functions for file names</a> <code class="language-plaintext highlighter-rouge">notdir</code> to extract the file name from the path (that is, <code class="language-plaintext highlighter-rouge">g100_clc00_V18_5.zip</code> from <code class="language-plaintext highlighter-rouge">data/raw/clc/g100_clc00_V18_5.zip</code>), and then <code class="language-plaintext highlighter-rouge">basename</code> to trim out the <code class="language-plaintext highlighter-rouge">.zip</code> extension.</p>

<p>In order to define an abstract computational workflow that is independent of the individual files<sup id="fnref:abstract-workflow" role="doc-noteref"><a href="#fn:abstract-workflow" class="footnote" rel="footnote">3</a></sup>, we can define the datasets to be processed in a separate variable, as in the snippet below:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">CLC_BASENAMES</span> <span class="o">=</span> g100_clc00_V18_5 g100_clc06_V18_5 g100_clc12_V18_5

<span class="nv">CLC_ZIP_FILEPATHS</span> <span class="o">:=</span> <span class="nf">$(</span><span class="nb">addsuffix</span> .zip, <span class="se">\</span>
    <span class="nf">$(</span><span class="nb">addprefix</span> <span class="nv">$(CLC_DIR)</span>/, <span class="nv">$(CLC_BASENAMES)</span><span class="nf">))</span>

<span class="nl">$(CLC_DIR)/%.zip</span><span class="o">:</span> <span class="nf">| $(CLC_DIR)</span>
	python src/data/download_clc.py <span class="nf">$(</span><span class="nb">notdir</span> <span class="nv">$@</span><span class="nf">)</span> <span class="nv">$@</span>

<span class="nl">download_clc</span><span class="o">:</span> <span class="nf">$(CLC_ZIP_FILEPATHS)</span>
</code></pre></div></div>

<p>Were we to consider an additional dataset in our analysis, only by adding it to <code class="language-plaintext highlighter-rouge">CLC_BASENAMES</code>, Make would be able to manage and execute our computational workflow accordingly. By applying the file name functions <code class="language-plaintext highlighter-rouge">addprefix</code> and <code class="language-plaintext highlighter-rouge">addsuffix</code> as shown above, <code class="language-plaintext highlighter-rouge">CLC_ZIP_FILEPATHS</code> will be a list of file paths (such as <code class="language-plaintext highlighter-rouge">data/raw/clc/g100_clc00_V18_5.zip</code>) for each base name in <code class="language-plaintext highlighter-rouge">CLC_BASENAMES</code>. Whenever we execute the Phony target <code class="language-plaintext highlighter-rouge">download_clc</code>, for each file path of <code class="language-plaintext highlighter-rouge">CLC_ZIP_FILEPATHS</code> <a href="https://www.gnu.org/software/make/manual/html_node/Pattern-Match.html#Pattern-Match">Make will detect that it matches the <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/%.zip</code> pattern</a> and proceed accordingly. Furthermore, since the next steps within the computational workflow do not require the zip files but the shapefiles and geotiff files that contain, we could <a href="https://www.gnu.org/software/make/manual/html_node/Chained-Rules.html">chain the pattern rules</a> as in:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">CLC_BASENAMES</span> <span class="o">=</span> g100_clc00_V18_5 g100_clc06_V18_5 g100_clc12_V18_5

<span class="nv">CLC_TIF_FILEPATHS</span> <span class="o">:=</span> <span class="nf">$(</span><span class="nb">addsuffix</span> .tif, <span class="se">\</span>
    <span class="nf">$(</span><span class="nb">addprefix</span> <span class="nv">$(CLC_DIR)</span>/, <span class="nv">$(CLC_TIF_FILENAMES)</span><span class="nf">))</span>

<span class="nl">$(CLC_DIR)/%.zip</span><span class="o">:</span> <span class="nf">| $(CLC_DIR)</span>
	python src/data/download_clc.py <span class="nf">$(</span><span class="nb">notdir</span> <span class="nv">$@</span><span class="nf">)</span> <span class="nv">$@</span>

<span class="nl">$(CLC_DIR)/%.tif</span><span class="o">:</span> <span class="nf">$(CLC_DIR)/%.zip</span>
	unzip <span class="nv">$&lt;</span> <span class="s1">'</span><span class="nf">$(</span><span class="nb">basename</span> <span class="nf">$(</span><span class="nb">notdir</span> <span class="s1">$@</span><span class="nf">))</span><span class="s1">.*'</span> <span class="nt">-d</span> <span class="nv">$(CLC_DIR)</span>
	<span class="nb">touch</span> <span class="nv">$@</span>

<span class="nl">download_clc</span><span class="o">:</span> <span class="nf">$(CLC_TIF_FILEPATHS)</span>
</code></pre></div></div>

<p>so that when executing <code class="language-plaintext highlighter-rouge">make clc</code>, Make first matches the <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/%.tif</code>, and determines that such geotiff target requires its corresponding zip, which will in turn be matched by the above rule with the <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/%.zip</code> target. Note that the <code class="language-plaintext highlighter-rouge">$&lt;</code> right after <code class="language-plaintext highlighter-rouge">unzip</code> is another automatic variable of Make, which corresponds to the first prerequisite of the rule in question (in this case, the path to zip file). Under these circumstances, if there is no further reference to <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/%.zip</code>, <a href="https://www.gnu.org/software/make/manual/html_node/Chained-Rules.html">Make will consider the zip files as intermediate</a> and will therefore remove them once the <code class="language-plaintext highlighter-rouge">$(CLC_DIR)/%.tif</code> targets have been produced. The same will not happen after extracting <code class="language-plaintext highlighter-rouge">$(GMB_SHP_FILEPATH)</code> from <code class="language-plaintext highlighter-rouge">$(GMB_ZIP_FILEPATH)</code> since the path to the zip file is stored in the latter variable and in such case Make does not remove it. We could achieve the same behavior by defining a pattern rule of the same form for GMB, even when we know that there is only a single file involved.</p>

<h4 id="metaprogramming-generating-rules-dynamically">Metaprogramming: generating rules dynamically</h4>

<p>Now comes the most convoluted part of our Makefile. In order to generate the urban extracts for each year and agglomeration, the script <code class="language-plaintext highlighter-rouge">src/data/make_urban_extract.py</code> needs the path to the GMB shapefile, a reference to the corresponding agglomeration (for instance, a slug as in <code class="language-plaintext highlighter-rouge">basel</code>) so that its boundaries can be extracted from the GMB shapefile (as detailed above, by filtering the values of the <code class="language-plaintext highlighter-rouge">ANAME</code> attribute column of the shapes), the CLC geotiff of the corresponding year, and a file path to dump the generated urban extract. We could make use of what we have seen so far and write a rule such as:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">URBAN_EXTRACTS_DIR</span> <span class="o">=</span> data/processed/urban_extracts

<span class="nl">$(URBAN_EXTRACTS_DIR)</span><span class="o">:</span>
	<span class="nb">mkdir</span> <span class="nv">$(URBAN_EXTRACTS_DIR)</span>
<span class="nl">$(URBAN_EXTRACTS_DIR)/basel-%.tif</span><span class="o">:</span> <span class="nf">$(CLC_DIR)/%.tif $(GMB_SHP_FILEPATH) </span>\
<span class="nf">    | $(URBAN_EXTRACTS_DIR)</span>
	<span class="nv">$(PYTHON_INTERPRETER)</span> src/data/make_urban_extract.py <span class="se">\</span>
	    <span class="nv">$(GMB_SHP_FILEPATH)</span> basel <span class="nv">$&lt;</span> <span class="nv">$@</span>
</code></pre></div></div>

<p>and so on for each agglomeration, but again, this would be hard to maintain as we include additional urban agglomerations. We will instead adopt a metaprogramming approach which consists in defining a variable with the list of the agglomeration slugs, and then dynamically generate a rule for each agglomeration as follows:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">AGGLOMERATION_SLUGS</span> <span class="o">=</span> basel geneve zurich

<span class="err">define</span> <span class="err">MAKE_URBAN_EXTRACT</span>
<span class="nl">$(URBAN_EXTRACTS_DIR)/$(AGGLOMERATION_SLUG)-%.tif</span><span class="o">:</span> \
<span class="nf">    $(CLC_DIR)/%.tif $(GMB_SHP_FILEPATH)| $(URBAN_EXTRACTS_DIR)</span>
	python src/data/make_urban_extract.py <span class="nv">$(GMB_SHP_FILEPATH)</span> <span class="se">\</span>
	    <span class="nv">$(AGGLOMERATION_SLUG)</span> <span class="nv">$$</span>&lt; <span class="nv">$$</span>@
<span class="err">endef</span>

<span class="err">$(foreach</span> <span class="err">AGGLOMERATION_SLUG,</span> <span class="err">$(AGGLOMERATION_SLUGS),</span> <span class="err">\</span>
    <span class="err">$(eval</span> <span class="err">$(MAKE_URBAN_EXTRACT)))</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">MAKE_URBAN_EXTRACT</code> works as a template which GNU Make’s <code class="language-plaintext highlighter-rouge">eval</code> function will use to dynamically generate rules. Note automatic variables within the template have been escaped through a preceding additional <code class="language-plaintext highlighter-rouge">$</code>. This is because the templates that we pass as argument to <code class="language-plaintext highlighter-rouge">eval</code> (in this case, <code class="language-plaintext highlighter-rouge">MAKE_URBAN_EXTRACT</code>) are expanded before we call them, so the variables that must be expanded during the execution of the rule (for instance, any automatic variable) should be escaped<sup id="fnref:metaprogramming" role="doc-noteref"><a href="#fn:metaprogramming" class="footnote" rel="footnote">4</a></sup>. The use of <a href="https://www.gnu.org/software/make/manual/html_node/Foreach-Function.html">Make’s <code class="language-plaintext highlighter-rouge">foreach</code> function</a> in order to dynamically generate a rule for each agglomeration is quite self-explanatory.</p>

<p>Subsequently, we will then use a nested loop to generate the full list of file paths of the form <code class="language-plaintext highlighter-rouge">data/processed/urban_extracts/{agglomeration}-{clc_basename}.tif</code> for each agglomeration and each CLC dataset, and create a Phony target to generate all the urban extracts:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">URBAN_EXTRACTS_TIF_FILEPATHS</span> <span class="o">:=</span> <span class="nf">$(</span><span class="nb">addprefix</span> <span class="nv">$(URBAN_EXTRACTS_DIR)</span>/, <span class="se">\</span>
    <span class="nf">$(</span><span class="nb">foreach</span> CLC_BASENAME, <span class="nv">$(CLC_BASENAMES)</span>, <span class="se">\</span>
        <span class="nf">$(</span><span class="nb">foreach</span> AGGLOMERATION_SLUG, <span class="nv">$(AGGLOMERATION_SLUGS)</span>, <span class="se">\</span>
            <span class="nv">$(AGGLOMERATION_SLUG)</span>-<span class="nv">$(CLC_BASENAME)</span>.tif<span class="nf">)))</span>

<span class="nl">urban_extracts</span><span class="o">:</span> <span class="nf">$(URBAN_EXTRACTS_TIF_FILEPATHS)</span>
</code></pre></div></div>

<p>Given how pattern rules work, Make will be able to match each of the file paths for urban extract to the rule that we have dynamically created for the corresponding agglomeration.</p>

<h3 id="detecting-edited-source-files">Detecting edited source files</h3>

<p>We will now proceed to the final part of the workflow, namely generating the figure and introduce another nice feature of Make while we are at it. There are many ways in which we can generate the figure from the urban extracts. We will use a script <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code> which will take as first argument the directory where the urban extracts are located, the output file path to store the figure as second argument, and three options allowing a list of values for the desired metrics, CLC basenames (each corresponding to a particular year) and the agglomeration slugs. The Python script will then deal with the association of each file path of the urban extracts to its respective agglomeration slug and CLC year.</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">.</span> <span class="err">.</span> <span class="err">.</span>

<span class="nv">FIGURE_FILEPATH</span> <span class="o">=</span> reports/figures/swiss-urbanization.png
<span class="nv">METRICS</span> <span class="o">=</span> proportion_of_landscape fractal_dimension_am

<span class="nl">$(FIGURE_FILEPATH)</span><span class="o">:</span> <span class="nf">$(URBAN_EXTRACTS_FILEPATHS)</span>
	python src/visualization/make_figure.py <span class="nv">$(URBAN_EXTRACTS_DIR)</span> <span class="se">\</span>
	    <span class="nv">$(FIGURE_FILEPATH)</span> <span class="nt">--metrics</span> <span class="nv">$(METRICS)</span> <span class="se">\</span>
	    <span class="nt">--clc-basenames</span> <span class="nv">$(CLC_BASENAMES)</span> <span class="se">\</span>
	    <span class="nt">--agglomeration-slugs</span> <span class="nv">$(AGGLOMERATION_SLUGS)</span>

<span class="nl">figure</span><span class="o">:</span> <span class="nf">$(FIGURE_FILEPATH)</span>
</code></pre></div></div>

<p>There is nothing tricky in the above snippet. Just note that we are using <code class="language-plaintext highlighter-rouge">$(URBAN_EXTRACTS_FILEPATHS)</code> as prerequisite and not <code class="language-plaintext highlighter-rouge">urban_extracts</code>, since as explained above, Phony targets are always executed even if the required files are up to date. Unless we changed the code, at this point running <code class="language-plaintext highlighter-rouge">make figure</code> will, from any starting conditions, proceed to the end of the workflow in the most efficient manner.</p>

<p>Let us now introduce the last feature of Make covered by this example case. Imagine that we have successfully generated our figure but we have changed the code of <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code> in order to (for instance) change the axis labels. We could remove the former figure, and execute <code class="language-plaintext highlighter-rouge">make figure</code> again. Nevertheless, this would still require manual interaction. Furthermore, let us assume that in the same way that we might need to update <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code>, other reasons require us to update an earlier of our workflow such as <code class="language-plaintext highlighter-rouge">src/data/make_urban_extract.py</code>. Under such circumstances, we would not need to manually remove a single figure but perhaps all the urban extracts. In order to let Make deal with such circumstances in an automated manner, we can set the file path to the script as a prerequisite of its corresponding rule. For instance, the latter snippet could be refactored to:</p>

<div class="language-make highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">...</span>
<span class="nv">MAKE_FIGURE_PY</span> <span class="o">=</span> src/visualization/make_figure.py

<span class="nl">$(FIGURE_FILEPATH)</span><span class="o">:</span> <span class="nf">$(URBAN_EXTRACTS_FILEPATHS) $(MAKE_FIGURE_PY)</span>
	python <span class="nv">$(MAKE_FIGURE_PY)</span> <span class="nv">$(URBAN_EXTRACTS_DIR)</span> <span class="nv">$(FIGURE_FILEPATH)</span> <span class="se">\</span>
	    <span class="nt">--metrics</span> <span class="nv">$(METRICS)</span> <span class="se">\</span>
	    <span class="nt">--clc-basenames</span> <span class="nv">$(CLC_BASENAMES)</span> <span class="se">\</span>
	    <span class="nt">--agglomeration-slugs</span> <span class="nv">$(AGGLOMERATION_SLUGS)</span>
</code></pre></div></div>

<p>Now whenever we edit <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code>, its modification time will be more recent than that of its respective target (that is, the figure file), and therefore if we execute <code class="language-plaintext highlighter-rouge">make figure</code>, Make will detect that the rule needs to be executed again. The above approach frees us from the duty of manually managing such situations, and might become crucial in other analysis cases, the amount of interim and processed data files might be in the range of hundreds, thousands or basically any number.</p>

<h2 id="concluding-remarks">Concluding remarks</h2>

<p>We have seen a thorough introduction to the use of GNU Make to manage computational workflows in geospatial data science. Here are some remarks from the perspective of someone coming from a Python background.</p>

<p>The main advantage of Make is its flexibility, which allows it to precisely manage any kind of computational workflow, from the compilation of source files to the example above. Nonetheless, this flexibility comes at the price of a syntax which is obviously less pleasant than Python’s, especially as we try to get the most out of Make and as a consequence Makefiles become inevitably abstract.</p>

<h3 id="avoid-hidden-state-let-make-not-python-carry-the-burden-of-managing-your-workflow">Avoid hidden state: let Make (not Python) carry the burden of managing your workflow</h3>

<p>While many (like myself) would feel more comfortable tackling the organization of a computational workflow in Python, it is important to delegate such burden to the Makefile. We have accordingly implemented our scripts so that they take the destination file path as a command-line argument. While the management of the input and output paths could have been handled within the Python scripts, under such circumstances Makefiles would have to make assumptions about how the script works. Avoiding this is in accordance to the idea of encapsulation of object-oriented programming as well as to a core Pythonic principle, namely “explicit is better than implicit”. The only distinction is that this time we must apply the foregoing principle to Make rather than to Python. <strong>An explicit control of the inputs an output files within Makefiles is crucial towards avoiding potential sources of hidden state</strong>.</p>

<p>In order to make the most out of Make, it is preferable that the Python scripts focus on the data processing tasks and are as agnostic of the project structure as possible. Overall, a sharp knowledge of the involved Python libraries combined with the flexibility of GNU Make should allow an efficient management of the computational workflow of most analysis cases.</p>

<h3 id="python-modules-and-detecting-edited-source-files">Python modules and detecting edited source files</h3>

<p>Another remark can be made with respect to how we have organized our scripts. We often organize Python code in a way that a <code class="language-plaintext highlighter-rouge">.py</code> file might perform multiple tasks, especially when we are using the <a href="https://palletsprojects.com/p/click/">Click</a> library to create CLI, which <a href="https://click.palletsprojects.com/en/7.x/commands/">allows a command to have a group of subcommands attached</a>.</p>

<p>In our example case, we could have implemented both <code class="language-plaintext highlighter-rouge">src/data/download_gmb.py</code> and <code class="language-plaintext highlighter-rouge">src/data/download_gmb.py</code> within a single Python module <code class="language-plaintext highlighter-rouge">src/data/download_data.py</code>. Nevertheless, if we set such single script as prerequisites of GMB and CLC respective rules (as we have shown above for <code class="language-plaintext highlighter-rouge">src/visualization/make_figure.py</code>), Make will always re-execute both rules when we edit its source, even if the amendments only concern one of the rules.</p>

<p>It is thus recommended that each <code class="language-plaintext highlighter-rouge">.py</code> file serves one and only one Make rule, even if that might sometimes feel strange for Python developers.</p>

<h3 id="alternatives-to-make">Alternatives to Make</h3>

<p>There are indeed alternatives to GNU Make which might be better suited for certain analysis<sup id="fnref:awesome-pipelines" role="doc-noteref"><a href="#fn:awesome-pipelines" class="footnote" rel="footnote">5</a></sup>. While many of them make reference to the fields of bioinformatics and machine learning, the same does not hold for geospatial data science. Nevertheless, most of the core principles apply similarly throughout all data-centric analysis cases.</p>

<p>My intention is to implement the management workflow of this post within <a href="https://snakemake.readthedocs.io/en/stable/">Snakemake</a>, which seems very aligned with Make while providing a more appealing syntax. On the other hand, before discovering the <a href="http://drivendata.github.io/cookiecutter-data-science/">Cookiecutter Data Science</a> project structure, I used <a href="https://github.com/iterative/dvc">dvc</a> in a geospatial analysis case. While it has the nice asset of employing checksums on the data files to ensure their consistency within the workflow (and different computers), and features a nice integration with git and S3 storage, <a href="https://github.com/iterative/dvc/issues/611">its CLI does not allow for a forthwith integration with the Cookiecutter Data Science project structure</a>. Nonetheless, there seems to be <a href="https://github.com/drivendata/cookiecutter-data-science/issues/158">some work in progress to overcome such drawback</a>.
On the other hand, I would also like to try the innovative approach of <a href="https://renku.readthedocs.io">Renku</a> (currently under early development stages), which appears similar to <a href="https://github.com/iterative/dvc">dvc</a> but more general, that is, less centered on machine learning workflows. Either way, I will try to come back with another post on this topic soon.</p>

<p>Regarding the research question of the example case, can we say that the landscapes of the main three Swiss agglomerations are becoming increasingly complex? Well, it is hard to say with this simplified example, but a more thorough manuscript is under preparation.</p>

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:i-dont-like-notebooks" role="doc-endnote">
      <p>See <a href="https://www.youtube.com/watch?v=7jiPeIFXb6U">the talk by Joel Grus at JupyterCon 2018</a> about Jupyter Notebooks and toxic habits <a href="#fnref:i-dont-like-notebooks" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:unzip-touch" role="doc-endnote">
      <p>The workaround of combining <code class="language-plaintext highlighter-rouge">touch</code> and <code class="language-plaintext highlighter-rouge">unzip</code> is borrowed from <a href="https://bost.ocks.org/mike/make/#syntax">Mike Bostock’s post on using Make for reproducible workflows</a> <a href="#fnref:unzip-touch" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:abstract-workflow" role="doc-endnote">
      <p>See also <a href="https://web.archive.org/web/20150206054212/http://www.bioinformaticszen.com/post/decomplected-workflows-makefiles/#abstraction-of-analysis-steps">Michael Barton’s post on reproducible workflows with GNU Make</a> <a href="#fnref:abstract-workflow" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:metaprogramming" role="doc-endnote">
      <p>For more details on how the <code class="language-plaintext highlighter-rouge">eval</code> function works, as well as more Make metaprogramming tricks see <a href="http://make.mad-scientist.net/the-eval-function/">this thorough post by Paul D. Smith</a> <a href="#fnref:metaprogramming" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:awesome-pipelines" role="doc-endnote">
      <p>See the <a href="https://github.com/pditommaso/awesome-pipeline">awesome-pipeline</a> repository for a thorough list of tools to manage computational workflows. <a href="#fnref:awesome-pipelines" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="python" /><category term="geospatial" /><category term="workflows" /><category term="make" /><summary type="html"><![CDATA[When I started using Jupyter Notebooks, about three years ago, I was astonished of how well that suited my iterative approach to both data analysis and software development. Nevertheless, probably like many others, I also embraced some toxic habits1. See the talk by Joel Grus at JupyterCon 2018 about Jupyter Notebooks and toxic habits &#8617;]]></summary></entry><entry><title type="html">Cities and the contemporary society: some thoughts from the complexity sciences</title><link href="https://martibosch.github.io/cities-contemporary-society-complexity-sciences/" rel="alternate" type="text/html" title="Cities and the contemporary society: some thoughts from the complexity sciences" /><published>2019-01-16T00:00:00+00:00</published><updated>2019-01-16T00:00:00+00:00</updated><id>https://martibosch.github.io/cities-contemporary-society-complexity-sciences</id><content type="html" xml:base="https://martibosch.github.io/cities-contemporary-society-complexity-sciences/"><![CDATA[<p>I have always found certain sadness underlying the second principle of thermodynamics. The first time that I learned about it was to know that it impedes us of building perpetual motion machines. Subsequently, high-school courses on physics and chemistry teach us how it is strongly tied to the notion of irreversibility. Formally, the second principle of thermodynamics establishes that the entropy of a closed system can only increase, and thus never decrease. But what is entropy?</p>

<p>The fact that we tend to associate entropy with disorder can be largely attributed to the legacy of Ludwig Boltzmann, the father of statistical mechanics. His interpretation of entropy can be clarified through the following example. Let’s consider we roll two dices. The image below illustrates all the 36 possible resulting configurations (microstates), grouped in columns according to the sum of the dices’ values (macrostates).</p>

<p><img src="/assets/images/complexity_society/entropy-dices.gif" alt="Illustration of Boltzmann's entropy" title="Image from http://hyperphysics.phy-astr.gsu.edu/hbase/Therm/entrop2.html" /></p>

<p>According to Boltzmann, the macrostate of maximum entropy is the one with most possible microstates. In our two-dice system, such macrostate corresponds to when the dices’ values add up to seven, given that there are six possible results that can bring us to such configuration. Instead, the two lateral ends of the image, that is, the macrostates where the dices’ values add up to two or twelve, correspond to the macrostates of minimal entropy, given that there is only one possible microstate (where we roll two ones or two sixes respectively).</p>

<p>Boltzmann was convinced that the macroscopic behavior of gases could be explained through the configurations formed by their molecules, quantifiable by such measure of entropy. Unfortunately, in a historical context where most of the scientific community did not believe in the existence of atoms, his contributions conflicted with many of his peers. In fact, apropos of the sadness of the second principle of thermodynamics mentioned above, it is worth pointing out that the constant criticisms on his work together with other personal circumstances lead Boltzmann to commit suicide during a family vacation in Trieste in 1906, just before the atoms became the zeitgeist of science during the twentieth century.</p>

<hr />

<p>The idea that when we repeatedly throw two dices, the most recurring result will be a a combination where the values add up to seven is consistent with the second principle of thermodynamics in that closed systems tend to its configurations of maximum entropy. Nevertheless, this is not the behavior displayed by many of the systems that we encounter in nature or the contemporary society.</p>

<p>An interesting example is the behavior observed in ant colonies. The image below (and in more detail in <a href="https://www.shutterstock.com/es/video/clip-7467553-giant-ant-colony-on-march">this video</a>) highlights that there exists strong collective order behind the movement of ant colonies.</p>

<p><img src="/assets/images/complexity_society/ant-colony.jpg" alt="Ant colony march" title="Image from https://www.shutterstock.com/es/video/clip-7467553-giant-ant-colony-on-march" /></p>

<p>From Boltzmann’s perspective, among all possible configurations, ant colonies tend to strongly ordered patterns, with very low entropy, and thus their occurrence should be very unlikely. Yet this is what we encounter.</p>

<p>Unlike with dices, the interactions between ants are not random, since they pursue collective objectives of locating and storing food. However, it remains surprising that, as suggested by a long line of investigations<sup id="fnref:ant-colony" role="doc-noteref"><a href="#fn:ant-colony" class="footnote" rel="footnote">1</a></sup>, the collective order observed in ant colonies emerges from local interactions within neighboring ants, with practically no global communication.</p>

<hr />

<p>As one might imagine, ant colonies are not the only system which, in apparent defiance of the second principle of thermodynamics, tends to generate ordered patterns. In fact, concurrent to the advances on the understanding of the behavior of ant colonies<sup id="fnref:brussels-collab" role="doc-noteref"><a href="#fn:brussels-collab" class="footnote" rel="footnote">2</a></sup>, the Nobel laureate in Chemistry Ilya Prigogine suggested that the reason why certain systems do not evolve towards states of maximum entropy is that the second principle of thermodynamics as well as Boltzmann’s probabilistic interpretation of entropy only apply to closed systems, that is, systems that do not interact with their environment.</p>

<p>There exists thus, a class of systems which Prigogine called ‘dissipative’, which as result of certain interactions with their environment, evolve towards ordered configurations<sup id="fnref:nouvelle-alliance" role="doc-noteref"><a href="#fn:nouvelle-alliance" class="footnote" rel="footnote">3</a></sup>. It seems reasonable to consider that cities - the subject of study of my PhD thesis - are part of this class of systems. A nice illustration of this kind of behavior that is applicable to contemporary cities is Schelling’s segregation model<sup id="fnref:thomas-schelling" role="doc-noteref"><a href="#fn:thomas-schelling" class="footnote" rel="footnote">4</a></sup>. Let’s consider a city with two kinds of inhabitants, the reds and the blues. As far as this example is concerned, the city will be represented by a grid of 40x40 cells, where 50% of them will be occupied by reds, 40% by blues and 10% empty. The central idea is very simple. Each inhabitant will consider his or her cell-location satisfactory if and only if at least four out of their eight neighbors are of the same color. In the opposite case, they will try to relocate to another cell. Starting from a random configuration, this mechanism will be applied until all agents are satisfied. The evolution of the city is illustrated below<sup id="fnref:schelling-mesa" role="doc-noteref"><a href="#fn:schelling-mesa" class="footnote" rel="footnote">5</a></sup>:</p>

<p><img src="/assets/images/complexity_society/schelling-animation.gif" alt="Schelling's model animation" title="Animation generated with https://github.com/martibosch/mesa-schelling-example" /></p>

<p>It is easy to see that in our city, analogously to ant colonies, a global order emerges from interactions that consider only local criteria. In the terminology of complex systems, these phenomena are said to exhibit <em>emergent properties</em>, which have not been directly incorporated within the design of the system but they have been an indirect result of their functioning - through a process of <em>self-organization</em>.</p>

<p>While Schelling’s model might be excessively simple and restricted to pedagogical purposes, contemporary cities also exhibit clear characteristics of self-organization and emerging order. A noteworthy example is the urban sprawl that we can observe in many Western cities, with Los Angeles, Atlanta or Houston as archetypal examples. The term urban sprawl has often been associated to pejorative connotations of disorganization and lack of planning.</p>

<p><img src="/assets/images/complexity_society/urban-sprawl.jpg" alt="Example of urban sprawl" title="Image from https://www.fastcompany.com/3028661/urban-sprawl-get-fat-stay-poor-and-die-in-car-crashes" /></p>

<p>Nonetheless, the commuting behavior of such sprawling cities reveal that, behind their haphazard visual impression, there is strong order underpinning their spatial development: despite notable suburban expansion of the largest metropolitan areas of the United States during the 80s and 90s, the commuting times have remained stable<sup id="fnref:commuting-paradox" role="doc-noteref"><a href="#fn:commuting-paradox" class="footnote" rel="footnote">6</a></sup>. At the same time, the number of commuting trips with both origin and destination in the suburbs increased substantially, which suggests that residences and businesses mutually co-locate<sup id="fnref:rational-locator" role="doc-noteref"><a href="#fn:rational-locator" class="footnote" rel="footnote">7</a></sup>. Therefore, while it is fair to consider that urban sprawl misuses space and induce automobile dependency, it is by no means a disordered phenomena. It is rather the result of a complex series of interactions mediated by many factors such as residential preferences, land value, existing infrastructure and the like.</p>

<p>To what extent systems that display self-organization and emerging properties are considered purely mechanical or some sort of animate organism is one of the fundamental questions of the last centuries of Western science and philosophy<sup id="fnref:emergence-determinism" role="doc-noteref"><a href="#fn:emergence-determinism" class="footnote" rel="footnote">8</a></sup>. In any case, the way in which the interactions between inhabitants can generate an emerging order without the intervention of any planning agent is a clear defiance of “planning” as a rational activity. This does not mean that any attempt to planning is futile but rather that most of the urban planning movements developed throughout the latest two centuries are not adequate for the kind of problems that contemporary cities pose. This is in fact the fundamental criticism of the journalist Jane Jacobs in her seminal book “The Death and Life of Great American Cities”, where she wrote:</p>

<blockquote>
  <p>Merely to think about cities and get somewhere, one of the main things to know is what kind of problem cities pose . . . .</p>

  <p>Cities happen to be problems in organized complexity, like the life sciences. They present “situations in which a half-dozen or even several dozen quantities are all varying simultaneously and in subtly interconnected ways . . . . The variables are many but they are not helter-skelter; they are “interrelated in to an organic whole.” . . . .</p>

  <p>The theorists of conventional modern city planning have consistently mistaken cities as problems of simplicity. . . and have tried to analyze and treat them thus<sup id="fnref:jacobs" role="doc-noteref"><a href="#fn:jacobs" class="footnote" rel="footnote">9</a></sup></p>
</blockquote>

<p>Similarly, and referring to the critique that the Marxist geographer and anthropologist David Harvey addresses to the models of classic urban economics, the also geographer Juval Portugali wrote the following:</p>

<blockquote>
  <p>[your models are] “incapable of saying anything of depth and profoundity . . .”; not because you are bad guys or second-rate professionals, but because like Don Quixote, you tilt at windmills, and by doing so not only do you not harm the real beast - the Capitalist mode of production - but you actually participate in its reproduction; not only that your sophisticated plans do not reduce injustice in the city, but they reproduce it; create it anew again and again. Capitalism with its city of injustice is advancing and flourishing behind the ideological false consciousness of your liberal science and planning[^portugali-harvey]</p>
</blockquote>

<p>Regardless of what one might think of capitalism, the emergent properties of complex systems such as cities and society in general, for instance the distribution of wealth or social mobility, are the result of the numerous interactions between the agents that constitute it, inevitably guided by certain economic, social and political system (whether it is capitalism, socialism or another). In fact, the notion of emergent properties resulting from a process of self-organization is salient on the “invisible hand” principle of Adam Smith, and liberal economists such as Friederich Hayek have actually built upon them to defend capitalism, considering the free market as the only mechanism capable of taking advantage of the dispersed knowledge present in our society.</p>

<p>In any case, it seems increasingly clear that conventional planning tools such as master plans are not enough to overthrow the emerging order that emanates from the actions and decisions of citizens in the context of capitalism, increasingly influenced by the globalization, volatility of the job market, technological advances and the incursion of platforms such as Airbnb and Uber[^planning-failures].</p>

<hr />

<p>Last summer I visited the Pueblos Mancomunados de la Sierra Norte de Oaxaca, in Mexico. It is a group of eight villages that conform a cooperative and community organization since pre-colonial times, which were formally constituted as an agrarian entity in 1961[^pueblos-mancomunados]. Their economic activities consist basically of agriculture, sustainable logging, and since 1995, a communal ecotourism program.</p>

<p><img src="/assets/images/complexity_society/mirador-torres-oaxaca.jpg" alt="'Amanecer' lookout, Llano Grande, Oaxaca" title="'Amanecer' lookout, Llano Grande, Oaxaca" /></p>

<p>One of the days, a local guide took us to the ‘Amanecer’ lookout to see the sunrise, and I took the opportunity to ask some questions about the organization of the communities. He explained us that any person, regardless of his or her nationality, can move there - provided that he or she agrees to follow the community rules. Each family is entitled to cut three pines in order to build their house, expansible for large families. Their local economy offers a series of jobs, however nobody is bound to take any specific job. The main duty as a citizen are the “cargos”, which consist of some basic tasks for the functioning of the communities, such as logging, organization of citizen and school boards, or jobs derived from the ecotourism program, among others. Every for years, a member of each house, aged between 18 and 65, must perform the “cargos” during a whole year as an unremunerated service to the community. On the other hand, it is worth pointing out that all the profits from the ecotourism program, which does not come cheap considering Oaxacan and Mexican prices, are entirely devoted to community projects. Altogether, not only their community system yields a fairly egalitarian distribution of wealth (which is relatively unsurprising), but also their local economy seems to enjoy good health.</p>

<p>At this point, it is easy to argue that under the functioning mode of the Pueblos Mancomunados, humanity would have never discovered subatomic particles, penicillin nor the field-effect transistor. I will not proceed into the evaluation of such remarks, since I do not intend to showcase the Pueblos Mancomunados as the universal model of an ideal society. The final reflection that I would like to raise is another. There is a perception, quite spread among our society, that the government positions are bound for people with high-level university education, since it is only with such background that one might be prepared for the arduous task of governing a system as complex as our society.</p>

<p>Nonetheless, it seems that such perception does not prevail so much for local governments, where other less-technical skills are more valued. Regarding this, I will conclude with an excerpt from Hayek, which leaving aside what one might think of his ideology, I find very pertinent to this discussion:</p>

<blockquote>
  <p>There is . . . a body of very important but unorganized knowledge which cannot possibly be called scientific . . . : the knowledge of the particular circumstances of time and place. It is with respect to this that practically every individual has some advantage over all others because he possesses unique information of which beneficial use might be made, but of which use can be made only if the decisions depending on it are left to him or are made with his active cooperation . . . Central planning based on statistical information by its nature cannot take direct account of these circumstances of time and place[^hayek-moroni]</p>
</blockquote>

<p>I hope that I have illustrated how in complex systems such as cities and the contemporary society, interventions do not always achieve the action-reaction effect that one might expect, not even when planned by experts with high academic qualifications. Therefore, I would like to raise a question concerning whether such interventions should be preferably managed by local governments, and by people that - no matter their academic training - know the particular circumstances of time and place of each neighborhood, village or city[^oaxaca-timezone], and to whom we can direct our concerns without the need to talk to three secretaries nor booking a flight to Madrid or Brussels. Although a network of local governments taking decisions that likely overlap between one city and another might seem inefficient when compared to a sole centralized government, according to Prigogine’s theory, it is in fact such dispersed knowledge and distribution of actions that allows complex systems to adapt to the changing circumstances of the world that surrounds us.</p>

<hr />

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:ant-colony" role="doc-endnote">
      <p>See J. L. Deneubourg et al. “Probabilistic Behaviour in Ants: A Strategy of Errors?” (1983). <a href="#fnref:ant-colony" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:brussels-collab" role="doc-endnote">
      <p>As a matter of fact, the connections between the contributions of Denenbourg and Prigogine’s teams are not a coincidence: their respective research groups were coexisted in the Université Libre de Bruxelles and carried out important academic exchanges. <a href="#fnref:brussels-collab" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nouvelle-alliance" role="doc-endnote">

      <p>More precisely, ‘dissipative’ systems use their energy inputs to generate internal order while exporting their entropy to the environment.</p>

      <p>Although Prigogine’s team developed the majority of their contributions within the context of thermodynamics, they realized very early that their discoveries were relevant for a broad group of scientific disciplines. This ambition is reflected in the title of one of their most influential works: “La Nouvelle Alliance. Métamorphose de la Science” (translated to English under the title “Order Out of Chaos: Man’s New Dialogue with Nature”). See I. Prigogine and I. Stengers “Order Out of Chaos: Man’s New Dialogue with Nature” (1984). <a href="#fnref:nouvelle-alliance" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:thomas-schelling" role="doc-endnote">
      <p>The name of the model comes from the Nobel laureate in economy Thomas Schelling. <a href="#fnref:thomas-schelling" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:schelling-mesa" role="doc-endnote">
      <p>The model’s parameters have been chosen according to computational criteria that make the animation more visually appealing. In any case, the model is deployed to <a href="https://schelling-mesa.herokuapp.com">https://schelling-mesa.herokuapp.com</a>, allowing for further experimentation with its parameters (its performance is not the best since I used <a href="https://www.heroku.com">Heroku</a>’s free tier). On the other hand, the source code is available in <a href="https://github.com/martibosch/mesa-schelling-example">https://github.com/martibosch/mesa-schelling-example</a>. <a href="#fnref:schelling-mesa" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:commuting-paradox" role="doc-endnote">
      <p>Between 1980 and 1985, the commuting times of the largest 20 metropolitan areas of the United States remained practically constant, without significant differences for sprawling cities such as Los Angeles, Houston or Atlanta. See P. Gordon et al. “The Commuting Paradox: Evidence from the Top Twenty” (1991). Between 1990 and 2000, the commuting times have grown with an elasticity of 0.1 with respect to the number of workers; that is, for a 10% growth on the number of workers, commuting times have grown only by 1%. See A. Anas “Discovering the Efficiency of Urban Sprawl” (2011). <a href="#fnref:commuting-paradox" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:rational-locator" role="doc-endnote">
      <p>See D. M. Levinson and A. Kumar “The Rational Locator: Why Travel Times Have Remained Stable” (1994). <a href="#fnref:rational-locator" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:emergence-determinism" role="doc-endnote">

      <p>The French mathematician Pierre-Simon Laplace developed the deterministic notion that if somebody possessed the ability of knowing, with an infinite degree of precision, the positions and forces of all the elements that constitute the universe, the evolution of the universe could be determined with infinite precision by means of Newton’s laws of motion. Nevertheless, Laplace and the advocates of their deterministic thesis recognized that such abilities might only be attributed to a superhuman entity, which popularized the construction of “Laplace’s demon”. The vision that the evolution of any system might be fully determined by means of Newton’s laws, independently of the arduous workload involved, was shared by most physicists until the general acceptance of the principles of quantum physics during the decade of 1920. In fact, half a century before that, after empirically linking the macroscopic behavior of gases to the collective motion of their molecules, Boltzmann already suggested that the interactions between molecules might not be determined exclusively by Netwon’s laws. More precisely, Boltzmann suggested that the energy levels of molecules could be discrete. It was in fact such idea, subsequently developed by Max Planck, which lead to the departure from classical physics and the ensuing commencement of quantum physics. See K. Popper “The Open Universe: An Argument for Indeterminism” (1982) and I. Prigogine i I. Stengers “Order Out of Chaos: Man’s New Dialogue with Nature” (1984).</p>

      <p>On the other hand, self-organization and emerging properties are central to the research on artificial life within the computational sciences.
The father of digital computing Alan Turing introduced with his homonymous machine in 1936 the idea of a computer where memory served to store not only data but also code instructions. Following this principle, the Hungarian mathematician John von Neumann speculated with the concept of self-replicant machines, which through a particular set of instructions stored in their memory, could be able to reproduce their structure. This idea lead to a whole research field called <em>Artificial Life</em>, which focuses on the exploration - through computational simulation - of systems related to natural life. Remarkable results within this field suggest that there is only a very limited set of conditions under which systems that interact with variable environments (such as cities) might generate persisting structures. Such structures display fractal order, where parts of the structure share similar properties with the whole. Fractal structures are very ubiquitous within living systems and nature in general, as for instance tree branches, circulatory systems, snowflakes or the colors of some animals, but also within artificial systems such as cities, transportation networks or even in the frequency of financial transactions. See C. G. Langton “Studying Artificial Life with Cellular Automata” (1986), R. White and G. Engelen “Cellular Automata and Fractal Urban Form: A Cellular Modelling Approach to the Evolution of Urban Land-Use Patterns”, P. Frankhauser “La fractalité des structures urbaines” (1994), M. Batty and P. Longley “Fractal Cities: A Geometry of Form and Function”, and M. Batty et al. “Urban Systems as Cellular Automata” (1997). <a href="#fnref:emergence-determinism" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jacobs" role="doc-endnote">

      <p>The excerpt is taken from the pages 431-435 of the book. The quotation marks are in reference to the essay that Warren Weaver wrote in the <em>1958 Annual Report of the Rockefeller Foundation</em>, where he listed three stages in the development of scientific thought: (I) the ability of treating problems of simplicity - where two directly related variables are considered, such as the pressure of a gas and its volume - during the seventeenth, eighteenth and nineteenth century, culminating in the foundations of the theories of light, sound, heat and electricity; (II) the ability to address problems of disorganized complexity, tractable throughout the methods of statistical mechanics developed by Boltzmann and others, which allow us to statistically approximate the speed and number of collisions between molecules or billiard balls; and finally (III) the ability to address problems of organized complexity, where the complexity does not lie with the number of variables but with their interconnections, in a way that the behavior of the system cannot be approximated by studying its parts in isolation. See J. Jacobs “The Death and Life of Great American Cities” (1961).
[^portugali-harvey]: The excerpt is taken from the page 34 of the book “Self-organization and the City” by Juval Portugali. The quotation marks are in reference to the book “Social Justice and the City”, by David Harvey. See D. Harvey “Social Justice and the City” (1973), and J. Portugali “Self-organization and the City” (1999).
[^planning-failures]: There is a whole research line devoted to the conformance evaluation of master land use plans. In most cases, significant differences exists between the zoning regulations and the actual land use, mostly attributed to small local amendments required to accommodate circumstances that were not predicted in the land use plans. See N. Alfasi et al. “The actual impact of comprehensive land-use plans: Insights from high resolution observations” (2012), T. Zhong et al. “Success or failure: Evaluating the implementation of China’s National General Land Use Plan (1997–2010)” (2014) and P. Abrantes et al. “Compliance of land cover changes with municipal land use planning: Evidence from the Lisbon metropolitan region (1990–2007)” (2016).
[^pueblos-mancomunados]: See https://www.gob.mx/conafor/prensa/pueblos-mancomunados-de-oaxaca-ejemplo-de-aprovechamiento-forestal-sostenible
[^hayek-moroni]: The excerpt is taken from the pages 80-83 of the book “Individualism and Economic Order” by Friederich Hayek. See F. A. Hayek “Individualism and Economic Order” (1948) and S. Moroni “Rethinking the theory and practice of land-use regulation: Towards nomocracy” (2010).
[^oaxaca-timezone]: An anecdotal yet interesting example of the self-management of the Pueblos Mancomunados is how, disregarding the Mexican central government, they do not apply any time change between summer and winter, given that their ancestors had never done so as there are no significant seasonal daylight differences in such latitude. <a href="#fnref:jacobs" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="blog" /><category term="cities" /><category term="society" /><category term="complexity" /><summary type="html"><![CDATA[I have always found certain sadness underlying the second principle of thermodynamics. The first time that I learned about it was to know that it impedes us of building perpetual motion machines. Subsequently, high-school courses on physics and chemistry teach us how it is strongly tied to the notion of irreversibility. Formally, the second principle of thermodynamics establishes that the entropy of a closed system can only increase, and thus never decrease. But what is entropy?]]></summary></entry><entry><title type="html">Les ciutats i la societat contemporània des de la teoria de la complexitat</title><link href="https://martibosch.github.io/ciutats-societat-contemporania-complexitat/" rel="alternate" type="text/html" title="Les ciutats i la societat contemporània des de la teoria de la complexitat" /><published>2019-01-08T00:00:00+00:00</published><updated>2019-01-08T00:00:00+00:00</updated><id>https://martibosch.github.io/ciutats-societat-contemporania-complexitat</id><content type="html" xml:base="https://martibosch.github.io/ciutats-societat-contemporania-complexitat/"><![CDATA[<p>El segon principi de la termodinàmica sempre m’ha evocat certa tristesa. El primer cop que en vaig prendre consciència va ser per aprendre que aquest ens impedeix construir un mòbil perpetu. Posteriorment, les classes de física i química a l’institut ens ensenyen com el segon principi està fortament lligat a la noció de la irreversibilitat. Formalment, el segon principi de la termodinàmica estableix que l’entropia d’un sistema aïllat només pot créixer, mai disminuir. Però, que és l’entropia?</p>

<p>El fet que associem el concepte d’entropia amb el desordre es deu majoritàriament al llegat de Ludwig Boltzmann, el pare de la mecànica estadística. La seva interpretació de l’entropia es pot clarificar amb l’exemple següent. Imaginem que llencem dos daus. La imatge a continuació ens il·lustra totes les 36 configuracions resultats possibles (microestats), agrupats segons la suma dels valors dels daus (macroestats).</p>

<p><img src="/assets/images/complexity_society/entropy-dices.gif" alt="Il·lustració del concepte d'entropia de Boltzmann" title="Imatge extreta de http://hyperphysics.phy-astr.gsu.edu/hbase/Therm/entrop2.html" /></p>

<p>Segons Boltzmann, el macroestat amb màxima entropia és aquell que té més microestats possibles. En el nostre sistema de dos daus, aquest macroestat correspon al cas on els valors dels daus sumen set, ja que hi ha sis resultats possibles que ens porten a aquesta configuració. En canvi, en els dos extrems laterals de la imatge, és a dir els macroestats on els valors dels daus sumen dos o dotze, corresponen als macroestats de mínima entropia, ja que només hi ha un microestat possible (on els daus marquen dos uns o dos sisos respectivament).</p>

<p>Boltzmann estava convençut de que les configuracions que formaven les molècules dels gasos, quantificables amb aquesta mesura d’entropia, determinaven el seu comportament macroscòpic. Malauradament, en un context històric en el qual la comunitat científico-acadèmica no creia en la existència dels àtoms, les seves contribucions discrepaven amb molts companys de professió. De fet, ja que començàvem parlant de la tristesa i el segon principi de la termodinàmica, val la pena dir que els constants qüestionaments al seu treball, conjuntament amb altres circumstàncies personals, van portar a Boltzmann a suïcidar-se durant unes vacances familiars a Trieste el 1906, tot just abans que els àtoms esdevinguessin el zeitgeist de la ciència durant el segle XX.</p>

<hr />

<p>La idea que si llencem aleatòriament dos daus repetides vegades, la configuració que trobarem en més ocasions serà aquella on els valors dels daus sumen set és consistent amb la idea del segon principi de la termodinàmica de que els sistemes tancats tendeixen a la màxima entropia possible. No obstant, aquest no és el comportament de molts dels sistemes que podem trobar en la natura o la societat contemporània.</p>

<p>Un exemple interessant és el comportament de les colònies de formigues. En la imatge a continuació (i amb més detall en <a href="https://www.shutterstock.com/es/video/clip-7467553-giant-ant-colony-on-march">aquest vídeo</a>) es pot observar com existeix un gran patró d’ordre col·lectiu en el moviment de les colònies de formigues.</p>

<p><img src="/assets/images/complexity_society/ant-colony.jpg" alt="Colònia de formigues en moviment" title="Imatge extreta de https://www.shutterstock.com/es/video/clip-7467553-giant-ant-colony-on-march" /></p>

<p>Des de la perspectiva de Boltzmann, entre totes les configuracions possibles, les colònies de formigues prenen formes molt ordenades, amb una entropia molt baixa, i per tant la seva ocurrència hauria de ser molt improbable. I tot i així aquestes són les formes que s’observen en la realitat.</p>

<p>A diferència dels daus, les interaccions entre formigues no són aleatòries, ja que responen a objectius col·lectius de localització i emmagatzematge de recursos alimentaris. Tanmateix, el que és sorprenent és que segons sembla indicar una llarga línia d’investigacions<sup id="fnref:ant-colony" role="doc-noteref"><a href="#fn:ant-colony" class="footnote" rel="footnote">1</a></sup>, les colònies de formigues aconsegueixen generar un ordre col·lectiu a partir d’interaccions locals entre formigues veïnes, pràcticament sense cap tipus de comunicació global.</p>

<hr />

<p>Com hom pot imaginar, les colònies de formigues no són l’únic sistema que, en aparent desafiament al segon principi de la termodinàmica, tendeixen a generar patrons ordenats. De fet, simultàniament als avenços en la comprensió de les colònies de formigues<sup id="fnref:brussels-collab" role="doc-noteref"><a href="#fn:brussels-collab" class="footnote" rel="footnote">2</a></sup>, el premi Nobel de química Ilya Prigogine va demostrar que el motiu pel qual certs sistemes no tendeixen cap a estats de màxima entropia, és que el segon principi de la termodinàmica i l’entropia de Boltzmann només apliquen a sistemes tancats, és a dir, a sistemes que no interactuen amb el medi que els envolta.</p>

<p>Existeix doncs, una classe de sistemes, anomenats ‘dissipatius’ segons Prigogine, que a través de certes interaccions amb el medi que els envolta, tendeixen cap a configuracions ordenades<sup id="fnref:nouvelle-alliance" role="doc-noteref"><a href="#fn:nouvelle-alliance" class="footnote" rel="footnote">3</a></sup>. Crec que és fàcil argumentar que les ciutats - el subjecte d’estudi de la meva tesi doctoral - formen part d’aquest grup. Una bona il·lustració de com podem trobar aquest tipus de comportaments en les ciutats contemporànies és el model de segregació de Schelling<sup id="fnref:thomas-schelling" role="doc-noteref"><a href="#fn:thomas-schelling" class="footnote" rel="footnote">4</a></sup>. Imaginem una ciutat amb dos tipus d’habitants, que denotarem respectivament amb els colors vermell i blau. Per aquest exemple, la ciutat serà una quadrícula de 40x40 caselles, on un 50% seran ocupades per habitants vermells, 40% per habitants blaus, i 10% buides. La idea central és molt senzilla. Els habitants estaran satisfets a la seva casella si com a mínim quatre dels seus vuit veïns són del mateix color. En el cas contrari, intentaran mudar-se a una altra casella. A partir d’una configuració inicial aleatòria, aquest mecanisme s’aplicarà fins a que tots els agents estiguin satisfets. A continuació podem veure l’evolució de la ciutat<sup id="fnref:schelling-mesa" role="doc-noteref"><a href="#fn:schelling-mesa" class="footnote" rel="footnote">5</a></sup>.</p>

<p><img src="/assets/images/complexity_society/schelling-animation.gif" alt="Animació del model de Schelling" title="Animació generada amb https://github.com/martibosch/mesa-schelling-example" /></p>

<p>Es pot observar clarament com la nostra ciutat, de manera anàloga a les colònies de formigues, genera un ordre global a partir d’interaccions que es regeixen per criteris exclusivament locals. En la terminologia de sistemes complexos, es diu que aquests fenòmens exhibeixen propietats emergents, que no han estat directament incorporades en el disseny del sistema però han estat un resultat indirecte del seu funcionament, a través d’un procés d’autoorganització. La simulació amb el model de <em>preferential attachment</em> que vaig descriure en <a href="https://www.fetasantfeliu.cat/opinio/101185/el-que-hi-ha-darrere-del-debat-urbanistic-de-sant-feliu">un article anterior sobre el debat urbanístic de Sant Feliu</a> és un altre exemple d’aquest tipus de processos.</p>

<p>Si bé el model de Schelling és excessivament simplista i poc aplicable fora de contextos pedagògics, en les ciutats contemporànies també podem trobar processos d’autoorganització i ordres emergents. Un exemple interessant és la dispersió urbana que podem observar en moltes ciutats occidentals, sobretot a Estats Units, com ara Los Angeles, Atlanta o Houston. En anglès, aquest tipus de dispersió s’anomena <em>urban sprawl</em>, i és sovint associat a connotacions negatives de desorganització i fracàs en la seva planificació urbana.</p>

<p><img src="/assets/images/complexity_society/urban-sprawl.jpg" alt="Exemple d'urban sprawl" title="Imatge extreta de https://www.fastcompany.com/3028661/urban-sprawl-get-fat-stay-poor-and-die-in-car-crashes" /></p>

<p>No obstant, darrere una impressió visual de desgavell, els hàbits de mobilitat en aquestes ciutats disperses evidencien que hi ha un clar ordre vigent en el seu desenvolupament espacial: tot i la considerable expansió suburbana de les majors àrees metropolitanes dels Estats Units durant els 80 i els 90, els respectius temps de viatge per anar a treballar s’han mantingut pràcticament constants<sup id="fnref:commuting-paradox" role="doc-noteref"><a href="#fn:commuting-paradox" class="footnote" rel="footnote">6</a></sup>.
Simultàniament, la quantitat de trajectes que tenen tant l’origen com la destinació en els suburbis ha incrementat de manera significativa, el qual suggereix que no només els habitants s’han establert en els suburbis, sinó que també ho han fet moltes empreses i les seves respectives oportunitats laborals<sup id="fnref:rational-locator" role="doc-noteref"><a href="#fn:rational-locator" class="footnote" rel="footnote">7</a></sup>. Així doncs, si bé podem considerar que aquest tipus de ciutats malbaraten l’espai i són ineficients en termes de la dependència en el cotxe que generen, en cap cas es tracta d’estructures desorganitzades, ans al contrari. Són el resultat d’un procés complex on hi intervenen preferències residencials, preus immobiliaris, infraestructures i molts altres factors.</p>

<p>Fins a quin punt considerem que els sistemes que exhibeixen processos d’autoorganització i propietats emergents són purament mecànics - o pel contrari, constitueixen algun tipus d’entitat més complexa - és una de les qüestions fonamentals dels darrers segles de la ciència i filosofia occidentals<sup id="fnref:emergence-determinism" role="doc-noteref"><a href="#fn:emergence-determinism" class="footnote" rel="footnote">8</a></sup>.
En qualsevol cas, la manera en la que les interaccions entre els habitants poden generar un ordre emergent sense la intervenció de cap agent extern és un clar desafiament a la “planificació” com a activitat racionalista.
Això no vol dir que qualsevol intent de planificació sigui completament estèril, si no que la major part dels corrents principals de l’urbanisme desenvolupats durant els dos últims segles no són adequats pel tipus de problemes que trobem en les ciutats contemporànies. Aquesta és de fet la crítica principal de la periodista Jane Jacobs en el seu influent llibre “The Death and Life of Great American Cities”, on va escriure:</p>

<blockquote>
  <p>Per pensar en ciutats i arribar a algun lloc, una de les principals qüestions que cal conèixer és quin tipus de problemes plantegen les ciutats . . . .</p>

  <p>Les ciutats resulten ser problemes de complexitat organitzada, com les ciències de la vida. Presenten “situacions en les que mitja dotzena o fins i tot diverses dotzenes de quantitats varien simultàniament i de maneres subtilment interconnectades . . . . Les variables són moltes, però no són un batibull; estàn “interrelacionades en un tot orgànic” . . . .</p>

  <p>Els teòrics de la planificació convencional de la ciutat moderna han malentès consistentment les ciutats com a problemes simples . . . i han intentat analitzar-los i tractar-los com a tals<sup id="fnref:jacobs" role="doc-noteref"><a href="#fn:jacobs" class="footnote" rel="footnote">9</a></sup></p>
</blockquote>

<p>De manera similar, i en referència a la crítica que el geògraf i antropòleg marxista David Harvey adreça als models clàssics d’economia urbana, el també geògraf Juval Portugali va escriure el següent:</p>

<blockquote>
  <p>[els vostres models són] “incapaços de dir res amb profunditat . . .”; no perquè sigueu males persones o professionals de segona, però perquè, com Don Quijote, esteu lluitant contra els molins de vent, i fent-ho no només no danyeu la bèstia real - el mode de producció Capitalista - sinó que de fet participeu en la seva reproducció; no només els vostres plans sofisticats no redueixen la injustícia a la ciutat, sinó que la reprodueixen; la tornen a crear novament una i altra vegada. El capitalisme amb la seva ciutat d’injustícia està avançant i florint darrere la falsa consciència ideològica de la vostra ciència i planificació liberal<sup id="fnref:portugali-harvey" role="doc-noteref"><a href="#fn:portugali-harvey" class="footnote" rel="footnote">10</a></sup></p>
</blockquote>

<p>Independentment del que cadascú pensi del capitalisme, les propietats emergents de sistemes complexos com les ciutats i la societat en general, com ara la distribució de la riquesa o la mobilitat social, són fruit de les interaccions entre els habitants que en formem part, inevitablement guiades per cert sistema econòmic, social i polític (tant si es tracta de capitalisme, socialisme o altres). De fet, la noció de propietats emergents resultants de processos d’autoorganització són clarament darrere el principi de la “mà invisible” d’Adam Smith, i economistes liberals com ara Friederich Hayek les han emprat justament per defensar el capitalisme, considerant el lliure mercat com l’únic mecanisme capaç d’aprofitar els coneixements dispersos presents en la societat.</p>

<p>En qualsevol cas, resulta cada cop més evident que els instruments convencionals de planificació, com ara els plans directors urbanístics, no són suficients per a suplantar l’ordre emergent que emana de les accions i decisions dels ciutadans en el context del capitalisme, cada cop més influenciades per la globalització, la creixent volatilitat del mercat laboral, constants desenvolupaments tecnològics i les irrupcions de plataformes com Airbnb o Uber<sup id="fnref:planning-failures" role="doc-noteref"><a href="#fn:planning-failures" class="footnote" rel="footnote">11</a></sup>.</p>

<hr />

<p>Aquest estiu vaig estar als Pueblos Mancomunados de la Sierra Norte de Oaxaca, a Mèxic. Es tracta d’un conjunt de vuit pobles organitzats de manera comunitària i cooperativa des de temps pre-hispànics, i que varen ser formalment constituïts en un únic nucli agrari el 1961<sup id="fnref:pueblos-mancomunados" role="doc-noteref"><a href="#fn:pueblos-mancomunados" class="footnote" rel="footnote">12</a></sup>. Les seves activitats econòmiques consisteixen bàsicament en la agricultura, l’explotació forestal sostenible, i des del 1995, un programa d’ecoturisme comunitari.</p>

<p><img src="/assets/images/complexity_society/mirador-torres-oaxaca.jpg" alt="Mirador 'amanecer', Llano Grande, Oaxaca" title="Mirador 'amanecer', Llano Grande, Oaxaca" /></p>

<p>Un dels dies un guia local ens va portar al Mirador ‘amanecer’ a veure sortir el sol, i vam tenir temps per fer-li unes quantes preguntes sobre el funcionament del poble. Ens va explicar que qualsevol persona, independentment de la nacionalitat, hi pot anar a viure - sempre i quant es segueixin les mateixes normes segons les quals es regeix la comunitat. Cada família té dret a tallar tres pins per a construir la seva casa, ampliable si el nombre de membres de la família així ho requereix. La seva economia local ofereix una sèrie de llocs de treball, però ningú està obligat a treballar en res en concret. La principal obligació com a ciutadà són els anomenats “cargos”, que consisteixen en tasques bàsiques pel funcionament de les comunitats, com ara la gestió forestal, organització juntes ciutadanes i escolars, o feines derivades del programa d’ecoturisme, entre d’altres. Cada quatre anys, un membre d’entre 18 i 65 anys de cada casa ha de realitzar “cargos” durant un any sencer de manera no remunerada. D’altra banda, cal esmentar que els ingressos provinents del seu programa d’ecoturisme, gens barat pel que és Oaxaca i Mèxic en general, es dediquen exclusivament a projectes comunitaris. Tot plegat, no només aquest sistema genera una distribució de la riquesa força igualitària entre els ciutadans (fet que no hauria de ser excessivament sorprenent), sinó que la seva economia local també gaudeix d’una bona salut.</p>

<p>Arribats aquí, és fàcil argumentar que amb l’organització dels Pueblos Mancomunados la humanitat mai hagués arribat a descobrir les partícules subatòmiques, la penicil·lina ni l’efecte transistor. No entraré a valorar aquestes qüestions, ja que la meva intenció no és posar als Pueblos Mancomunados com a model universal de com hem d’estructurar la nostra societat. La reflexió final que voldria plantejar és una altra.
Hi ha una percepció, força estesa entre la nostra societat, que els càrrecs de les administracions públiques han de ser ocupats per persones d’altes formacions universitàries, ja que són aquestes les úniques preparades per la difícil tasca de governar un sistema tant complex com la societat contemporània.
Sembla però, que aquesta percepció es dilueix en la política local, on es valoren més altres capacitats menys tècniques.
Sobre aquesta qüestió, acabaré amb una citació de Hayek, que independentment del que es pugui pensar de la seva ideologia, trobo que és molt pertinent en aquest context:</p>

<blockquote>
  <p>Hi ha . . . tot un conjunt molt important de coneixement desorganitzat que en cap cas es pot anomenar científic . . . : el coneixement de les circumstàncies particulars de moment i lloc. És en aquest sentit que pràcticament cada persona té algun avantatge respecte als altres ja que posseeix informació única de la qual se’n pot fer un ús beneficiós, però tal ús només és possible quan les decisions de les quals depèn les pren la persona en qüestió o es prenen amb la seva cooperació activa . . . La planificació centralitzada basada en informació estadística, per la seva pròpia naturalesa, no pot prendre en compte directament aquestes circumstàncies de moment i lloc.<sup id="fnref:hayek-moroni" role="doc-noteref"><a href="#fn:hayek-moroni" class="footnote" rel="footnote">13</a></sup></p>
</blockquote>

<p>Espero haver il·lustrat que en sistemes tant complexos com les ciutats i societats contemporànies, les intervencions no sempre obtenen l’efecte acció-reacció que s’espera, ni tan sols quan les planegen persones amb importants formacions acadèmiques.
Així doncs, m’agradaria plantejar si no seria millor que aquestes intervencions es gestionessin des del món local, per part persones que coneixen que coneixen les circumstàncies particulars de cada barri, poble o ciutat<sup id="fnref:oaxaca-timezone" role="doc-noteref"><a href="#fn:oaxaca-timezone" class="footnote" rel="footnote">14</a></sup>, i a les que podem adreçar-nos quan quelcom no funciona com previst, sense necessitat de parlar amb tres secretàries per prendre una cita ni de desplaçar-nos a Madrid o Brussel·les.
Si bé una xarxa de governs locals prenent decisions que possiblement es solapen entre poble i poble pot semblar ineficient comparat amb una administració centralitzada, segons la teoria de Prigogine, és justament aquesta distribució de coneixement i accions la que permet als sistemes complexos adaptar-se a les circumstàncies canviants del món que ens envolta.</p>

<hr />

<h2 id="notes">Notes</h2>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:ant-colony" role="doc-endnote">
      <p>Vegeu J. L. Deneubourg et al. “Probabilistic Behaviour in Ants: A Strategy of Errors?” (1983). <a href="#fnref:ant-colony" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:brussels-collab" role="doc-endnote">
      <p>De fet, les connexions entre les contribucions dels grups de Deneubourg i Prigogine no són casualitat: els seus respectius grups de recerca eren contemporanis en la mateixa Université Libre de Bruxelles i realitzaven importants intercanvis acadèmics. <a href="#fnref:brussels-collab" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:nouvelle-alliance" role="doc-endnote">

      <p>Més concretament, els sistemes ‘dissipatius’ tendeixen a aprofitar les seves entrades d’energia per a generar ordre indern tot exportant-ne l’entropia a l’exterior.</p>

      <p>Malgrat que la majoria de les contribucions de l’equip de Prigogine es van desenvolupar en el context de la termodinàmica, van adonar-se’n ràpidament que els seus descobriments eren rellevants per a un ampli grup de disciplines científiques. Aquesta ambició queda reflexada en el títol d’un dels seus treballs més influents: “La Nouvelle Alliance. Métamorphose de la Science” (traduït a l’anglès sota el títol “Order Out of Chaos: Man’s New Dialogue with Nature”). Vegeu I. Prigogine i I. Stengers “Order Out of Chaos: Man’s New Dialogue with Nature” (1984). <a href="#fnref:nouvelle-alliance" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:thomas-schelling" role="doc-endnote">
      <p>El nom del model ve del premi Nobel d’economia Thomas Schelling. <a href="#fnref:thomas-schelling" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:schelling-mesa" role="doc-endnote">
      <p>Els paràmetres del model han estan triats segons criteris computacionals que fan la animació més adequada per a il·lustrar-ne el seu funcionament. En tot cas, es pot accedir a la implementació del model i experimentar amb diferents paràmetres a <a href="https://schelling-mesa.herokuapp.com">https://schelling-mesa.herokuapp.com</a> (la velocitat d’execució és millorable ja que l’aplicació està allotjada en un servidor segons les prestacions gratuïtes de <a href="https://www.heroku.com">Heroku</a>). D’altra banda, el codi del model és accessible a <a href="https://github.com/martibosch/mesa-schelling-example">https://github.com/martibosch/mesa-schelling-example</a>. <a href="#fnref:schelling-mesa" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:commuting-paradox" role="doc-endnote">
      <p>Entre el 1980 i el 1985, els temps de viatge de les vint majors àrees metropolitanes dels Estats Units es van mantenir pràcticament constants, sense diferències significatives per a ciutats més físicament disperses com ara Los Angeles, Houston o Atlanta. Vegeu P. Gordon et al. “The Commuting Paradox: Evidence from the Top Twenty” (1991). Entre el 1990 i el 2000, els temps de viatge han crescut amb una elasticitat de 0.1 respecte el nombre de treballadors, és a dir, per un creixement del nombre de treballadors del 10%, els temps de viatge només han crescut 1%. Vegeu A. Anas “Discovering the Efficiency of Urban Sprawl” (2011). <a href="#fnref:commuting-paradox" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:rational-locator" role="doc-endnote">
      <p>Vegeu D. M. Levinson i A. Kumar “The Rational Locator: Why Travel Times Have Remained Stable” (1994). <a href="#fnref:rational-locator" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:emergence-determinism" role="doc-endnote">

      <p>El matemàtic francès Pierre-Simon Laplace va desenvolupar la noció determinista que si algú tingués capacitat de conèixer, amb precisió infinita, les posicions i velocitats de tots els elements que composen l’univers, l’evolució de l’univers es podria deduir amb tot detall a partir de les lleis universals de la física formulades per Isaac Newton. Tanmateix, Laplace i els defensors de les seves tesis deterministes van reconèixer que tals capacitats només podien ser atribuïdes a una entitat sobrehumana, el qual va acaba popularitzant la construcció del “Dimoni de Laplace”. La visió que l’evolució de qualsevol sistema es pot predir mitjançant exclusivament les lleis de Newton, independentment la laboriosa càrrega de treball que això requereixi, va ser compartida per la majoria de físics fins a la adopció generalitzada dels principis de la física quàntica durant la dècada del 1920. De fet, mig segle abans, després de relacionar empíricament les propietats macroscòpiques dels gasos amb el comportament col·lectiu de les seves molècules, Boltzmann ja havia suggerit que les interaccions entre molècules podien no regir-se per les lleis del moviment Newton. Més concretament, Boltzmann va suggerir que els nivells d’energia de les molècules podien ser discrets. Va ser justament aquesta idea, posteriorment desenvolupada per Max Planck, la que va acabar marcant la ruptura amb la física clàssica i el consegüent naixement de la física quàntica. Vegeu K. Popper “The Open Universe: An Argument for Indeterminism” (1982) i I. Prigogine i I. Stengers “Order Out of Chaos: Man’s New Dialogue with Nature” (1984).</p>

      <p>D’altra banda, la autoorganització i les propietats emergents són centrals en les investigacions sobre la vida artificial en les ciències de la computació.
El pare de la computació digital Alan Turing va introduir amb la seva homònima màquina universal el 1936 la idea d’un ordinador on la memòria no només servia per a emmagatzemar-hi dades, sinó també instruccions. Seguint aquest principi, el matemàtic hongarès John von Neumann va desenvolupar el concepte de màquines autoreplicants, les quals a través de cert set d’instruccions emmagatzemades en la seva memòria, serien capaces de reproduir la seva estructura. Aquesta idea va donar peu a tota una línia de recerca anomenada <em>Artificial Life</em>, que es centrava en la exploració a través de simulacions dels sistemes artificials que exhibeixen propietats similars als éssers vius. Resultats destacables en aquest camp semblen indicar que només hi ha certes condicions en les que els sistemes que interactuen amb medis variables (com ara les ciutats) aconsegueixen generar estructures persistents. Tals estructures es caracteritzen per un ordre fractal, on parts de l’estructura tenen formes similars a l’estructura complerta. Les estructures fractals són molt abundants en els sistemes vius i la naturalesa en general, com ara les branques dels arbres, l’aparell circulatori, flocs de neu, o els colors d’alguns animals, però també entre sistemes artificials com ara les ciutats, xarxes de transport o inclús en les distribucions de transaccions en els mercats financers. Vegeu C. G. Langton “Studying Artificial Life with Cellular Automata” (1986), R. White i G. Engelen “Cellular Automata and Fractal Urban Form: A Cellular Modelling Approach to the Evolution of Urban Land-Use Patterns”, P. Frankhauser “La fractalité des structures urbaines” (1994), M. Batty i P. Longley “Fractal Cities: A Geometry of Form and Function”, i M. Batty et al. “Urban Systems as Cellular Automata” (1997). <a href="#fnref:emergence-determinism" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:jacobs" role="doc-endnote">

      <p>El text original de l’extret citat es troba entre pàgines 431-435 del llibre “The Death and Life of Great American Cities” de Jane Jacobs, i és el següent:</p>

      <blockquote>
        <p>Merely to think about cities and get somewhere, one of the main things to know is what kind of problem cities pose . . . .</p>

        <p>Cities happen to be problems in organized complexity, like the life sciences. They present “situations in which a half-dozen or even several dozen quantities are all varying simultaneously and in subtly interconnected ways . . . . The variables are many but they are not helter-skelter; they are “interrelated in to an organic whole.” . . . .</p>

        <p>The theorists of conventional modern city planning have consistently mistaken cities as problems of simplicity. . . and have tried to analyze and treat them thus</p>
      </blockquote>

      <p>Les cometes que utilitza Jane Jacobs són en referència a l’assaig que Warren Weaver va escriure en el <em>1958 Annual Report of the Rockefeller Foundation</em>, on s’enumeraven tres etapes de desenvolupament en la història del pensament científic: (I) l’habilitat d’adreçar problemes simples - aquells que consideren dues variables directament relacionades, com ara la pressió d’un gas i el seu volum - durant els segles XVII, XVIII i XIX, culminant amb l’establiment dels fonaments de les teories de la llum, so, calor i electricitat; (II) l’habilitat d’adreçar problemes de complexitat desorganitzada, tractables amb els mètodes de mecànica estadística desenvolupats per Boltzmann i altres, i que ens permeten per exemple aproximar estadísticament les velocitats i nombres de col·lisions entre molècules o boles de billar; i finalment (III) l’habilitat d’adreçar problemes de complexitat organitzada, on la complexitat no rau tant en el nombre de variables com en les seves interconnexions, de manera que el comportament del sistema no es pot aproximar estudiant-ne separadament les seves parts. Vegeu J. Jacobs “The Death and Life of Great American Cities” (1961). <a href="#fnref:jacobs" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:portugali-harvey" role="doc-endnote">

      <p>El text original de l’extret citat es troba a la pàgina 34 del llibre “Self-organization and the City” de Juval Portugali, i és el següent:</p>

      <blockquote>
        <p>[your models are] “incapable of saying anything of depth and profoundity . . .”; not because you are bad guys or second-rate professionals, but because like Don Quixote, you tilt at windmills, and by doing so not only do you not harm the real beast - the Capitalist mode of production - but you actually participate in its reproduction; not only that your sophisticated plans do not reduce injustice in the city, but they reproduce it; create it anew again and again. Capitalism with its city of injustice is advancing and flourishing behind the ideological false consciousness of your liberal science and planning</p>
      </blockquote>

      <p>Les cometes són en referència al llibre “Social Justice and the City” de David Harvey. Vegeu D. Harvey “Social Justice and the City” (1973), i J. Portugali “Self-organization and the City” (1999). <a href="#fnref:portugali-harvey" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:planning-failures" role="doc-endnote">
      <p>Hi ha tota una línia de recerca dedicada a l’evaluació de la implementació dels plans generals d’ús de sòl. En la majoria de casos, s’observen importants diferències entre les assignacions d’ús de sòl dels plans i el seu ús real, atribuïts en gran part a petites modificacions locals requerides per a acomodar canvis que no estaven previstos en el moment de la confecció dels plans. Vegeu N. Alfasi et al. “The actual impact of comprehensive land-use plans: Insights from high resolution observations” (2012), T. Zhong et al. “Success or failure: Evaluating the implementation of China’s National General Land Use Plan (1997–2010)” (2014) i P. Abrantes et al. “Compliance of land cover changes with municipal land use planning: Evidence from the Lisbon metropolitan region (1990–2007)” (2016). <a href="#fnref:planning-failures" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:pueblos-mancomunados" role="doc-endnote">
      <p>Vegeu https://www.gob.mx/conafor/prensa/pueblos-mancomunados-de-oaxaca-ejemplo-de-aprovechamiento-forestal-sostenible <a href="#fnref:pueblos-mancomunados" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:hayek-moroni" role="doc-endnote">

      <p>El text original de l’extret citat es troba entre les pàgines 80-83 del llibre “Individualism and Economic Order”, i és el següent:</p>

      <blockquote>
        <p>There is . . . a body of very important but unorganized knowledge which cannot possibly be called scientific . . . : the knowledge of the particular circumstances of time and place. It is with respect to this that practically every individual has some advantage over all others because he possesses unique information of which beneficial use might be made, but of which use can be made only if the decisions depending on it are left to him or are made with his active cooperation . . . Central planning based on statistical information by its nature cannot take direct account of these circumstances of time and place</p>
      </blockquote>

      <p>Vegeu F. A. Hayek “Individualism and Economic Order” (1948) i S. Moroni “Rethinking the theory and practice of land-use regulation: Towards nomocracy” (2010). <a href="#fnref:hayek-moroni" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:oaxaca-timezone" role="doc-endnote">
      <p>Un exemple anecdòtic però tanmateix interessant de fins a quin punt els Pueblos Mancomunados s’autogestionen és com, a diferència del govern central, no s’hi aplica cap canvi d’hora entre l’estiu i l’hivern, ja que tradicionalment mai ho havien fet, i de fet a la latitud on es troben no hi ha diferències estacionals significatives en el nombre d’hores de llum natural. <a href="#fnref:oaxaca-timezone" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>martibosch</name></author><category term="fait-a-sant-feliu" /><category term="ciutats" /><category term="societat" /><category term="complexitat" /><summary type="html"><![CDATA[El segon principi de la termodinàmica sempre m’ha evocat certa tristesa. El primer cop que en vaig prendre consciència va ser per aprendre que aquest ens impedeix construir un mòbil perpetu. Posteriorment, les classes de física i química a l’institut ens ensenyen com el segon principi està fortament lligat a la noció de la irreversibilitat. Formalment, el segon principi de la termodinàmica estableix que l’entropia d’un sistema aïllat només pot créixer, mai disminuir. Però, que és l’entropia?]]></summary></entry><entry><title type="html">L’auto de Junqueras, l’hermenèutica i la pluralitat del poder judicial a l’estat espanyol</title><link href="https://martibosch.github.io/auto-junqueras-hermeneutica-pluralitat-poders-estat/" rel="alternate" type="text/html" title="L’auto de Junqueras, l’hermenèutica i la pluralitat del poder judicial a l’estat espanyol" /><published>2018-01-08T00:00:00+00:00</published><updated>2018-01-08T00:00:00+00:00</updated><id>https://martibosch.github.io/auto-junqueras-hermeneutica-pluralitat-poders-estat</id><content type="html" xml:base="https://martibosch.github.io/auto-junqueras-hermeneutica-pluralitat-poders-estat/"><![CDATA[<p>L’èxit de l’Internet durant el segle XXI ens ha portat en una situació on usuaris de xarxes socials i blogs es poden dedicar, de manera lucrativa, a comentar assumptes de gran complexitat com el canvi climàtic o els conflictes a l’Orient Mitjà amb poc més coneixement i rigor que el que pot aportar un cunyat nostre qualsevol durant la sobretaula de Sant Esteve.
Per a intentar evitar convertir-me en un d’aquests usuaris, m’abstindré de comentar <a href="https://ep00.epimg.net/descargables/2018/01/05/d3a29b435b42e89a937ab529845b5c2a.pdf">l’auto segons el qual es manté la presó preventiva d’Oriol Junqueras</a>, ja que el meu coneixement sobre el dret penal és molt limitat, per no dir inexistent.</p>

<p>Els quatre anys que vaig dedicar a estudiar Enginyeria Industrial em van acostumar a un tipus de problemes en els quals existeix una solució òptima que es pot trobar a través de mètodes estrictament matemàtics. Malauradament, aquestes situacions no es donen a l’hora d’intentar resoldre els problemes d’una societat cada cop més complexa.
Actualment, com a candidat a doctor en enginyeria civil i ambiental, em centro en problemes de gestió del territori i urbanisme on s’utilitzen mapes, previsions i simulacions realitzades a partir de models que per més que ens esforcem, no poden ser més que una representació simplificada d’una realitat massa complicada com per traduir-la en un sistema d’equacions.</p>

<p>És en aquesta bretxa entre els límits del coneixement humà i la complexitat de la realitat on entra en joc l’<em>hermenèutica</em> - l’art de la interpretació de fenòmens a través d’una representació de coneixement, sigui aquesta un text sagrat, jurídic o un mapa resultant d’una simulació. Inevitablement, en estudis de fenòmens complexos com ara les ciutats del segle XXI, l’hermenèutica juga un paper clau des del principi: quines hipòtesis es formulen, quins elements es consideren rellevants i quins s’obvien, o com se n’interpreten els resultats. Això fa que un dia llegeixi articles d’acadèmics prestigiosos que defensen densificar les ciutats per a reduir l’ús del cotxe privat, i l’endemà en llegeixi d’altres on acadèmics d’igual prestigi defensen l’expansió urbana per a reduir les congestions a les carreteres. Aquestes interpretacions estan fortament influenciades per la personalitat, el context sociocultural i la ideologia política de cadascú. I per més que no m’agradi, el que pugui presentar a la meva tesi doctoral també estarà influenciat per aquests factors.</p>

<p>Com hom pot imaginar, l’hermenèutica juga un paper central en la disciplina del dret: les sentències judicials depenen de com els jutges interpreten les accions dels acusats i el codi penal. Per l’auto de Junqueras esmentat anteriorment, he pogut llegir una magistrada <a href="https://twitter.com/LuisamarGG/status/949314926623019009">al·legant que impulsar als independentistes a la mobilització implicava també impulsar-los a enfrontaments físics amb les forces de l’ordre</a>, a la vegada que tal raonament és qualificat com <a href="http://www.eldiario.es/zonacritica/puede-acabar-bien_6_726587345.html">“esperpéntico” per part d’un catedràtic de dret constitucional</a> o com a possible causant d’una sentència del Tribunal Europeu de Drets Humans <a href="https://twitter.com/miguelpasquau/status/949371548686868481">segons un altre magistrat</a>.</p>

<p>Davant el fet que el codi penal pugui donar peu a tal diversitat d’interpretacions, i considerant la influència del context personal de cadascú sobre aquestes, sembla que una manera d’evitar biaixos sistemàtics en les decisions judicials seria garantir una pluralitat sociocultural i ideològica en el sí del poder judicial. Si ens fixem doncs en els tres magistrats firmants de l’auto en qüestió, veiem que un ve d’una família que porta quatre generacions exercint en l’administració de la Justícia, un altre és nét d’un magistrat del Tribunal Suprem i l’últim és el fill del rector de la Universitat de Santiago que va investir a Franco com doctor honoris causa el 1965. Tot sigui dit, la família no es tria, i l’arbre genealògic no hauria d’impedir a ningú la pràctica de cap professió. No obstant cal destacar que durant la transició espanyola <a href="http://www.iniciativacomunista.org/opinion-debate/863-jueces-del-franquismo-a-la-democracia">molts magistrats de l’antic Tribunal de Orden Público van ser automàticament recol·locats en les noves estructures de poder judicial, i com en nombroses ocasions es van permetre arxivar casos i deixar en llibertat a autors d’assassinats ocorreguts ja durant la transició</a>. Així doncs, sembla que l’esmentada composició de la sala del Tribunal Suprem que manté a Junqueras a la presó no és una casualitat. De fet, el periodista d’investigació Alfredo Grimaldos, autor del llibre <a href="http://www.edicionespeninsula.com/es/llibre/claves-de-la-transicion-1973-1986-para-adultos_23369.html">‘Claves de la transición 1973-1986 para adultos’</a>, afirma que <a href="http://www.publico.es/politica/transicion-cimiento-podredumbre-actual.html">“El sistema judicial actual es de extrema derecha”</a>.</p>

<p>Reitero doncs, tornant al principi, que molts periodistes, politòlegs, influents usuaris de Twitter o jo mateix, no tenim els els coneixements necessaris per a valorar l’auto de Junqueras. El que sí que em permeto afirmar, és la importància que té l’hermenèutica en les resolucions judicials. Així doncs, més enllà dels anàlisis amateurs d’interlocutòries, potser el que cal és qüestionar si davant el ventall tant ampli d’interpretacions emanant d’experts en dret penal que podem trobar, l’evidència de quines resolucions acaba emetent el poder judicial espanyol permet afirmar que existeix un biaix sistemàtic contra certes ideologies o minories regionals. I aquí potser em precipito, però davant el continuisme del poder franquista durant la transició, la tortura a militants independentistes, els ja 420 dies de presó preventiva als joves d’Altsasu, la complicitat de les forces de l’ordre amb atemptats i agressions per part l’extrema dreta, o com el Ministerio de Justícia ignora exhorts de la Justícia Argentina per a investigar alts càrrecs del franquisme i la transició… crec que hi ha quelcom que fa pudor.</p>]]></content><author><name>martibosch</name></author><category term="fait-a-sant-feliu" /><category term="epistemologia" /><category term="transició" /><category term="procés" /><summary type="html"><![CDATA[L’èxit de l’Internet durant el segle XXI ens ha portat en una situació on usuaris de xarxes socials i blogs es poden dedicar, de manera lucrativa, a comentar assumptes de gran complexitat com el canvi climàtic o els conflictes a l’Orient Mitjà amb poc més coneixement i rigor que el que pot aportar un cunyat nostre qualsevol durant la sobretaula de Sant Esteve. Per a intentar evitar convertir-me en un d’aquests usuaris, m’abstindré de comentar l’auto segons el qual es manté la presó preventiva d’Oriol Junqueras, ja que el meu coneixement sobre el dret penal és molt limitat, per no dir inexistent.]]></summary></entry><entry><title type="html">El que hi ha darrere del debat urbanístic de Sant Feliu</title><link href="https://martibosch.github.io/el-que-hi-ha-darrere-el-debat-urbanistic-de-sant-feliu/" rel="alternate" type="text/html" title="El que hi ha darrere del debat urbanístic de Sant Feliu" /><published>2017-04-30T00:00:00+00:00</published><updated>2017-04-30T00:00:00+00:00</updated><id>https://martibosch.github.io/el-que-hi-ha-darrere-el-debat-urbanistic-de-sant-feliu</id><content type="html" xml:base="https://martibosch.github.io/el-que-hi-ha-darrere-el-debat-urbanistic-de-sant-feliu/"><![CDATA[<p>La meva família va arribar a Sant Feliu al 1995, quan <a href="http://www.idescat.cat/">segons l’Idescat hi vivien 36736 persones</a>. Recordo quan anava a l’escola Salvador Espriu i jugant a futbol al pati no es veia cap edifici més que les fàbriques del polígon Matacàs. Quan l’any 2011 vaig tornar-hi per a fer de monitor de l’activitat extraescolar de futbol sala, em va produir una sensació amarga veure el pati envoltat d’edificis de set o vuit plantes que li fan ombra durant l’hivern. De manera similar, gent de la meva edat de l’escola Bon Salvador m’havia explicat algun cop com abans de la construcció de la ciutat esportiva Joan Gamper, des de la seva escola a Sant Joan Despí no hi havia més que descampats.</p>

<p>Aquests records no haurien d’estranyar massa a ningú, ja que a data del 2016 Sant Feliu comptava amb 44086 habitants, el qual representa un creixement del 20% respecte el 1995. Hom pot entendre doncs, que l’ampliació de la superfície urbana de Sant Feliu és una mera resposta a un creixement demogràfic, que de fet ha estat inferior al 26% corresponent a la mitjana catalana. El debat més calent que hi ha sobre la taula, segons les opinions que he llegit, és la manera en la que aquests nous edificis contribueixen a fer Sant Feliu una ciutat dormitori.</p>

<p>El que intento explicar a continuació, és com la transformació que porta a Sant Feliu cap a una ciutat dormitori no és més que el resultat d’un procés demogràfic que es manifesta arreu del món en diverses magnituds i variants, però compartint gran part dels mateixos fonaments.</p>

<h2 id="com-es-distribueix-la-població-catalana">Com es distribueix la població catalana?</h2>

<p>L’arrel del problema s’amaga sota les distribucions estadístiques de la població a Catalunya. Per a començar amb un exemple, mireu el gràfic següent:</p>

<p><img src="/assets/images/urban_complexity/height_weight_distplot.png" alt="Distribució d'alçades i pesos" title="Distribució d'alçades i pesos" class="center-image" /></p>

<p>A l’esquerra i a la dreta es mostren respectivament les distribucions d’alçada i pes d’un grup d’individus, segons <a href="http://wiki.stat.ucla.edu/socr/index.php/SOCR_Data_Dinov_020108_HeightsWeights">una enquesta de joves de 18 anys a Hong Kong</a>. Veiem doncs que la gran majoria d’individus de l’estudi mesuren entre 165 i 180 centímetres i pesen entre 45 i 70 kilograms, amb mitjanes entre els 170-175cm i 55-60kg respectivament. El que s’observa correspon una <em>distribució normal</em>, que és raonablement com estem acostumats a pensar quan parlem de números. Per exemple, quan comparàvem notes a la classe de l’institut, probablement la mitjana estava entre el 5 i el 6, mentre que la majoria tenia notes entre el 3 i el 7. Desprès quedaven els pocs estudiants que havien tret entre un 1 i un 2 i a l’altre extrem també pocs estudiants que tenien un 9 o un 10.</p>

<p>Si ara observem com es distribueixen els habitants en els diferents municipis de Catalunya segons l’Idescat, ens trobem amb el següent:</p>

<p><img src="/assets/images/urban_complexity/municipal_population_distplot.png" alt="Distribució de poblacions municipals de Catalunya" title="Distribució de poblacions municipals de Catalunya" /></p>

<p>La diferència entre aquesta distribució i les d’alçada i pes mostrades abans són evidents. La gran majoria de municipis es situen a l’esquerra del gràfic, amb poblacions molt petites, mentre que hi ha grup pràcticament inapreciable de municipis de més de 200000 habitants. El que s’observa correspon a una distribució <em>lognormal</em> <a href="https://github.com/martibosch/martibosch.github.io/blob/master/assets/notebooks/urbanism_sant_feliu.ipynb">amb una cua aproximable per una <em>power-law</em> segons la <em>Llei de Zipf</em></a>. El lingüista estatunidenc George Kingsley Zipf va popularitzar aquesta llei a l’observar que en la gran majoria de paraules d’un llenguatge s’utilitzen rarament, ja que hi ha un grup reduït de paraules que utilitzem molt sovint. Hi ha doncs una clara analogia amb el nostre gràfics: la gran majoria de municipis són poc poblats, mentre que hi ha un grup petit de municipis amb molts habitants. De fet, aquest comportament es replica en molts altres fenòmens de la ciència, com ara sistemes biològics, termodinàmics o conjunts fractals en matemàtiques. Una pràctica habitual per analitzar millor aquests fenòmens és representar-los en escala logarítmica. Si ho apliquem al nostre gràfic, trobem el següent:</p>

<p><img src="/assets/images/urban_complexity/municipal_population_log_distplot.png" alt="Distribució logarítmica de poblacions municipals de Catalunya" title="Distribució logarítmica de poblacions municipals de Catalunya" /></p>

<p>S’observa així que la majoria de les poblacions tenen entre 100 i 10000 habitants, amb un únic municipi de més d’un milió d’habitants situat a l’extrem dret, que com bé podem imaginar correspon a la ciutat de Barcelona. Malgrat ser tant recurrents, el que sobta d’aquestes distribucions és la dificultat que ens comporta intentar predir o extrapolar-ne conclusions de manera intuïtiva. Per exemple, sabent que mesuro 177cm, tots seríem més o menys capaços d’estimar intuïtivament i amb relativa precisió que un 25% de les persones de la meva edat deuen ser més alts que jo. En canvi, malgrat que ens diguin que a Barcelona hi ha el 21% dels habitants de Catalunya, segurament no sabrem estimar tant fàcilment quin percentatge dels Catalans viuen en les 5 ciutats més grans (són un 33%, entre Barcelona, l’Hospitalet de Llobregat, Badalona, Terrassa i Sabadell).</p>

<h2 id="què-ens-ha-portat-a-aquesta-desproporció">Què ens ha portat a aquesta desproporció?</h2>

<p>Qualsevol persona que conegui més o menys Catalunya es pot imaginar que el creixement demogràfic no s’ha repartit de manera simètrica. Probablement, els habitants de tercera edat la vida dels quals ha transcorregut en els petits pobles de la Catalunya interior ens podrien explicar el progressiu èxode dels seus fills i néts cap a poblacions més grans de la comarca o directament cap a Barcelona. Aquesta migració succeïx de manera retroactiva: com que les poblacions més grans ofereixen més oportunitats, més habitants nous hi arriben, que a la vegada fan créixer el municipi, que passa a oferir encara més oportunitats.</p>

<p>Aquest comportament es coneix com a avantatge acumulatiu o <em>preferential attachment</em>, i es pot resumir en l’aforisme de “els més rics es tornen més rics, i els més pobres més pobres”. La formalització d’aquest fenomen s’atribueix a les observacions del matemàtic escocès Udny Yule sobre el fet que els gèneres biològics amb més espècies són també els que tenen més capacitat de generar noves espècies.</p>

<p>Veiem un exemple de com funciona el <em>preferential attachment</em>, il·lustrat amb una xarxa d’amics, on cada node representa una persona, i un enllaç entre dos nodes simbolitza que les persones corresponents són amics. La situació inicial, il·lustrada en la imatge a continuació, és una persona central que és l’única amistat dels altres 9 membres.</p>

<p><img src="/assets/images/urban_complexity/preferential_attachment_01.png" alt="Estat inicial de la xarxa" title="Estat inicial de la xarxa" /></p>

<p>Per a simular el <em>preferential attachment</em>, farem que arribin noves persones a la xarxa, que intentaran fer-se amics d’algú que ja tingui molts amics. Els detalls exactes de la implementació estan descrits <a href="http://ccl.northwestern.edu/netlogo/models/PreferentialAttachment">en la documentació del programa utilitzat</a>. Veiem doncs com evoluciona el sistema quan hi ha 25, 50, 100, 250 i 500 persones respectivament:</p>

<p><img src="/assets/images/urban_complexity/preferential_attachment.gif" alt="Evolució de la xarxa segons el &quot;preferential attachment&quot;" title="Evolució de la xarxa segons el &quot;preferential attachment&quot;" /></p>

<p>Podem observar doncs, que una idea molt senzilla com el <em>preferential attachment</em> pot generar patrons clarament estructurats. Les imatges de la xarxa probablement ens recordin a diferents fenòmens: enllaços entre pàgines web, amistats de Facebook, connexions entre Aeroports, xarxes de proteïnes en biologia o matèria condensada en física. Aquest tipus de xarxes agrupen sota el nom de <em>scale-free networks</em>, i comparteixen propietats molt similars en camps completament diferents de la ciència. Les xarxes que formen les ciutats i pobles, no en són una excepció.</p>

<p>Com es pot observar, a mesura que evoluciona el sistema, la persona central acapara gran part de les amistats. De fet, la distribució del numero d’amics que té cada persona de la xarxa, (representada en el gràfic de “Degree Distribution”) es va assemblant cada cop més a la distribució en municipis dels habitants de Catalunya. Al cap i a la fi, les decisions que guien l’evolució de les ciutats segueixen el mateix raonament que els nouvinguts a la xarxa: quan decidim mudar-nos a una ciutat, la majoria busquem aquelles que ens ofereixin més oportunitats, de la mateixa manera que els nouvinguts busquen fer-se amics d’aquells que tenen més amics.</p>

<h2 id="làrea-metropolitana-de-barcelona-i-lurban-sprawl">L’Àrea Metropolitana de Barcelona i l’<em>urban sprawl</em></h2>

<p>Segons el raonament del <em>preferential attachment</em>, la ciutat de Barcelona hauria d’haver acaparat la majoria del creixement de Catalunya durant els últims anys. Però si mirem com ha evolucionat la població de les comarques de Catalunya en termes absoluts des del 1981 al 2011 segons l’Idescat, podrem observar com no ha estat així.</p>

<iframe width="100%" height="640" frameborder="0" src="https://martibosch.carto.com/builder/f215ab9d-c7f8-4577-9d2f-7f371519f254/embed" allowfullscreen="" webkitallowfullscreen="" mozallowfullscreen="" oallowfullscreen="" msallowfullscreen=""></iframe>

<p>Veiem doncs que el Barcelonès és justament la comarca que més població ha perdut, concretament 208211 habitants. En canvi, les comarques de l’Àrea Metropolitana de Barcelona (AMB) i el Tarragonès són les que més han crescut. La hipòtesi més versemblant seria que ja no queda espai per allotjar nous residents al Barcelonès, i per tant aquests s’estableixen a les comarques de l’AMB. Però no només no s’ha pogut allotjar nous residents, sinó que hi ha hagut una pèrdua de població significativa. Això implica que força residents del Barcelonès que han decidit marxar-hi, molt possiblement per a establir-se en poblacions veïnes de l’AMB.</p>

<p>Aquesta migració cap a les rodalies normalment s’atribueix a les preferències per un context residencial més tranquil, espaiós i verd, evitant la congestió de les ciutats sense allunyar-se’n excessivament. La seva prevalença en el context nord americà ha popularitzat globalment el terme <em>urban sprawl</em>, que es refereix al model urbà de zones residencials disperses, separades dels centres d’activitats d’oci i comercials, sovint forçant als seus habitants a una forta dependència del transport motoritzat. Probablement això ens faci pensar a localitats del Vallès, el Maresme o del Baix Llobregat, com ara Sant Cugat, Cerdanyola, el Masnou, Mongat, Vallirana, Corbera, i també a barris Santfeliuencs recents com ara Les Grasses, Mas Lluhí o els nous habitatges al costat de la Ciutat Esportiva Joan Gamper.</p>

<h2 id="implicacions-a-lescala-sant-feliu-i-de-catalunya">Implicacions a l’escala Sant Feliu i de Catalunya</h2>

<p>El que descriu aquest article doncs, es pot recapitular en les següents observacions:</p>

<ul>
  <li>la distribució demogràfica de Catalunya està molt descompensada, amb una minoria de municipis concentrant la major part de la seva població</li>
  <li>aquesta distribució és resultat d’on procés retroalimentat que tendeix a desproporcionar-la encara més (el <em>preferential attachment</em>), amb Barcelona com a gran centre de gravetat</li>
  <li>la saturació de la ciutat de Barcelona, i les preferències per un context residencial més tranquil, han desencadenat una migració dels seus habitants cap a altres municipis de l’AMB</li>
</ul>

<p>Com hom pot imaginar, aquestes observacions són extrapolables a la majoria de regions desenvolupades del món: segons <a href="https://esa.un.org/unpd/wup/">les previsions United Nations</a>, la proporció mundial de persones vivint en ciutats ha passat d’un 3% al 1800 a un 50% actual, amb estimació d’arribar al 60% al 2030. Seguint l’actual tendència global cap a l’<em>urban sprawl</em>, <a href="https://www.theguardian.com/cities/2016/jul/12/urban-sprawl-how-cities-grow-change-sustainability-urban-age">s’espera que la superfície total ocupada per aglomeracions es tripliqui els propers 40 anys</a>.</p>

<p>Considerant la magnitud d’aquest procés demogràfic, cal entendre que probablement hi ha poques solucions implementables des de l’ajuntament de Sant Feliu. Els municipis metropolitans estem condemnats a jugar un paper clau en aquesta transformació, i per a no acabar sent satèl·lits invisibles de Barcelona, cal canviar la percepció territorial de Catalunya. Com remarca el periodista Marc Andreu en <a href="http://www.elcritic.cat/blogs/sentitcritic/2016/05/19/les-ciutats-invisibles-de-la-catalunya-metropolitana/">un extret del seu llibre <em>Les ciutats invisibles</em></a>, “Que en el concepte periodístic i polític de territori, tant interioritzat a Catalunya, no s’hi incloguin les realitats metropolitanes només s’explica com una vacuna mal receptada contra el centralisme de Barcelona”. D’altra banda, potser també són necessàries altres respostes a tal centralisme, com ara solucions a l’èxode juvenil dels pobles de la Catalunya oblidada <a href="http://www.elcritic.cat/blogs/sentitcritic/2016/02/05/guia-per-a-catalans-que-baixareu-el-7-f-a-la-colonia-de-mes-enlla-de-lebre/">“d’on als 18 anys marxes a estudiar i rarament hi tornes”</a>, ja que en molts casos tals joves no s’estableixen a l’AMB per voluntat pròpia, sinó per manca d’oportunitats en la seva comarca d’origen.</p>

<p>Però inclús acceptant que la migració cap a l’AMB és inevitable, cal plantejar quin tipus de teixit urbà acomodarà aquest creixement. Un <a href="http://www.sciencedirect.com/science/article/pii/S0169204607002848">estudi de l’Universitat Autònoma de Barcelona</a> va monitoritzar les transformacions de l’AMB entre el 1993 i el 2000, destacant com la proliferació de zones industrials i comercials segregades junt amb la privatització d’espais públics ens estan acostant cada cop més al model urbà nord americà. Ara bé, l’estudi distingeix una característica peculiar de l’AMB: l’existència de centres vitals a les afores de Barcelona ha frenat notablement l’<em>urban sprawl</em>, ja que sembla que els seus habitants tenim més preferència per habitar a prop d’aquests centres, a diferència dels països anglosaxons. El que potser podem fer des de Sant Feliu doncs, és aprofitar aquesta diferència per a evitar acabar sent una ciutat dormitori: <a href="http://www.fetasantfeliu.cat/opinio/100126/sant-feliu-ciutat-invisible-a-la-recerca-de-la-cohesio-social">com deia l’Arnau Picón al Fet a Sant Feliu</a>, “Més enllà de les polítiques municipals necessàries, comença a ser hora que la ciutat s’espavili. Els qui més hi hem de fer som les entitats, els comerços i els ciutadans”.</p>

<p>No entraré a valorar quines són les decisions de l’ajuntament en clau urbanística que ens poden portar al model de ciutat dormitori o d’<em>urban sprawl</em>. Ara bé, el que sí que hem de reclamar tots els Santfeliuencs és el nostre dret a participar activament en aquest debat i la presa de decisions, ja que votar un cop cada quatre anys no és suficient per a hipotecar el model de ciutat que volem per als propers anys.</p>]]></content><author><name>martibosch</name></author><category term="fait-a-sant-feliu" /><category term="urbanisme" /><summary type="html"><![CDATA[La meva família va arribar a Sant Feliu al 1995, quan segons l’Idescat hi vivien 36736 persones. Recordo quan anava a l’escola Salvador Espriu i jugant a futbol al pati no es veia cap edifici més que les fàbriques del polígon Matacàs. Quan l’any 2011 vaig tornar-hi per a fer de monitor de l’activitat extraescolar de futbol sala, em va produir una sensació amarga veure el pati envoltat d’edificis de set o vuit plantes que li fan ombra durant l’hivern. De manera similar, gent de la meva edat de l’escola Bon Salvador m’havia explicat algun cop com abans de la construcció de la ciutat esportiva Joan Gamper, des de la seva escola a Sant Joan Despí no hi havia més que descampats.]]></summary></entry><entry><title type="html">La Desconeguda Història dels dos Joves Santfeliuencs que varen introduir el Reggaeton a Catalunya</title><link href="https://martibosch.github.io/la-historia-reggaeton-catalunya/" rel="alternate" type="text/html" title="La Desconeguda Història dels dos Joves Santfeliuencs que varen introduir el Reggaeton a Catalunya" /><published>2017-03-06T00:00:00+00:00</published><updated>2017-03-06T00:00:00+00:00</updated><id>https://martibosch.github.io/la-historia-reggaeton-catalunya</id><content type="html" xml:base="https://martibosch.github.io/la-historia-reggaeton-catalunya/"><![CDATA[<p>En Miguel Carrasco, <em>Carra</em> per als amics, és un graduat en Direcció Hotelera, natural de l’Hospitalet de Llobregat. Nascut al 1992, va arribar a Sant Feliu el 2006 ja que feia més d’un any que cursava l’educació secundària a l’Escola Mestral. En Martí Bosch estudià Enginyeria Industrial, i posteriorment un màster en Intel.ligència Artificial. Igual que en Miguel, va néixer l’any de les Olimpíades, però ell va arribar a la <em>Ciutat de Les Roses</em> als 3 anys, per a començar el parvulari al Salvador Espriu.</p>

<p>Avui es retroben a Seattle, al nord-oest dels Estats Units: en Miguel hi viu des de fa gairebé un any, ja que treballa a l’Hotel Sheraton; en Martí en canvi, tot just hi és de passada com a part d’un viatge que realitza a la Costa Oest abans de començar un doctorat a Suïssa. Des d’un café del modern barri de Capitol Hill, ens expliquen la desconeguda història de com vàren fer arribar el <em>Reggaeton</em> (i altres músiques llatines) a Catalunya.</p>

<h4 id="malgrat-haver-crescut-els-dos-a-les-rodalies-de-barcelona-proveniu-dentorns-molt-diferents-com-va-ser-la-vostra-infància-abans-de-conèixer-vos">Malgrat haver crescut els dos a les rodalies de Barcelona, proveniu d’entorns molt diferents. Com va ser la vostra infància abans de conèixer-vos?</h4>

<p><strong>M.C.</strong> Jo vaig créixer en un entorn on predominava la llengua castellana, molts dels meus amics eren del Madrid i el català era només una assignatura a l’escola. Quasi tots érem descendents de famílies procedents d’altres regions d’Espanya, predominantment de <em>el Sur</em>, en el meu cas de Múrcia i Rus, a Jaén. Probablement per això, la comunió era inqüestionable, tots portàvem els penjolls amb la creu, i sentíem els daltabaixos de <em>la Roja</em> com a nostres.</p>

<p><strong>M.B.</strong> En l’arbre genealògic de la meva família, només hi podem recordar cognoms catalans, però sempre ha estat l’únic cas en la meva classe. Havent anat a l’escola pública al barri Falguera, vaig viure (en menor mesura) un ambient semblant al que explica en Miguel. D’altra banda, els caps de setmana feia vida principalment al <em>barri centre</em>, on el teixit associatiu ha contribuït sempre al fet que les tradicions d’arrel més catalana fossin sempre presents en els seus ciutadans. Formant part d’aquests dos móns em semblava increïble com conviviem tots en la mateixa ciutat d’una manera tant impermeable.</p>

<h4 id="a-catalunya-ja-fa-uns-anys-que-a-les-festes-majors-i-locals-doci-hi-predomina-la-música-llatina-com-us-sentiu-després-dhaver-aconseguit-aquesta-gesta">A Catalunya ja fa uns anys que a les festes majors i locals d’oci hi predomina la música llatina. Com us sentiu després d’haver aconseguit aquesta gesta?</h4>

<p><strong>M.C.</strong> A casa i al carrer, em vaig criar escoltant estils de música llatina i espanyola. En aquell llavors a l’Hospitalet, ningú de la nostra edat havia escoltat mai cançons de Lluís Llach o Marina Rossell. Per mi no és més que veure com allò amb el que he viscut des de petit s’ha extès a la resta del País. Tot i estar orgullós d’haver assumit la responsabilitat d’extrendre i fer entendre el <em>Reggaeton</em> a les poblacions amb més tradició catalana, no puc evitar sentir certa indignació al veure com els joves ara en gaudeixen sense preguntar-se quant va costar, ni qui se’n va fer càrrec.</p>

<p><strong>M.B.</strong> Les úniques cançons castellanes que havien tingut presència al cotxe de la meva família eren “Y cómo es él” i “Me llamas” de José Luís Perales. Nosaltres escoltàvem Els Pets o Lax’n’Busto (mai Sopa de Cabra perquè no soportàvem en Gerard Quintana). Per això estic sorprès d’haver estat involucrat d’aquesta manera en el procés d’acceptació la música llatina a Catalunya, encara que he d’admetre que si no fos pel <em>Carra</em> mai l’hagués ni tan sols considerat.</p>

<h4 id="heu-utilitzat-expressions-com-fer-entendre-i-procés-dacceptació-en-el-context-duna-música-molt-popular-que-sescolta-arreu-del-món-no-utilitzeu-termes-molt-exagerats">Heu utilitzat expressions com “fer entendre” i “procés d’acceptació” en el context d’una música molt popular que s’escolta arreu del món. No utilitzeu termes molt exagerats?</h4>

<p><strong>M.C.</strong> Ni molt menys, deixa’m que et posi uns quants exemples. Quan vaig arribar a Sant Feliu al 2006, cada vegada que intentava posar una cançó de <em>Reggaeton</em> se’m titllava de <em>cani</em>, <em>xungo</em> i se’n reien dient “com es nota que ets de <em>Hospi</em>”. Això va seguir així durant anys, de fet quan vaig conèixer al Martí el 2008 res havia canviat. És més, ell va ser el que més se’n va riure de mi per el sol fet de que m’agradés aquesta música.</p>

<p><strong>M.B.</strong> És cert, de fet jo sentia vergonya aliena per el <em>latineo</em>, el seu ritme descarat i les seves lletres <em>dantesques</em>. No va ser fins a l’estiu del 2010, quan amb la família estàvem de vacances a Paraguai que vem arribar a escoltar-ne una cançó sencera: a la ràdio va sonar “Noche de Sexo”, i al trobar-nos a l’Amèrica Llatina vem trobar pertinent deixar-la sonar. Tot i que no em va agradar, és cert que la vaig percebre de manera diferent.</p>

<p><img src="/assets/images/entrevista_reggaeton/entrevista_reggaeton_01.jpg" alt="Foto #1" title="Fotos de Mireia Planas Sucarrat" /></p>

<blockquote>
  <p>cada vegada que intentava posar una cançó de <em>Reggaeton</em> se’m titllava de <em>cani</em>, <em>xungo</em> i se’n reien dient “com es nota que ets de <em>Hospi</em>”</p>
</blockquote>

<h4 id="llavors-si-en-aquell-moment-tampoc-et-va-agradar-què-et-va-fer-canviar-dopinió">Llavors, si en aquell moment tampoc et va agradar, què et va fer canviar d’opinió?</h4>

<p><strong>M.B.</strong> De fet va ser tot plegat casualitat. Poc després de tornar d’aquelles vacances, estàvem bevent a casa meva abans de sortir al Milenio, i el <em>Carra</em> va intentar posar una de les seves cançons. Tothom va reaccionar contràriament. A mi en canvi, em va recordar a la cançó que havia escoltat feia unes setmanes, i li vaig proposar que si la coneixia, la posés.</p>

<p><strong>M.C.</strong> Evidentment la vaig reconèixer immediatament, però no la tenia al MP3, i en aquella època ningú tenia internet al mòbil. Per això li vaig proposar de demanar-la un cop arribéssim al Milenio. Però vem ser massa innocents de pensar que tot seria tant senzill. Jo sabia que no m’anaven a prendre seriosament ja que no hagués estat ni molt menys la primera vegada que demanés una cançó. Però pensàvem que seria diferent si la demanava en Martí…</p>

<p><strong>M.B.</strong> Justament l’estiu del 2010 vàrem passar amb els amics moltes tardes mandroses de cafè a l’Ateneu, en les quals sovint el gerent del Milenio, en Miquel Albert s’autoconvidava a la nostra taula. Ell ens explicava què tenia preparat per aquell cap de setmana (per exemple, nits de Mojitos o Xupitos especials), i amb els amics hi anàvem i hi consumíem. De forma que vaig creure que no era agosarat anar a demanar una cançó, encara que fós d’un gènere tan controvertit. Al cap i a la fi, quan sortim de nit anem a divertir-nos, no?</p>

<h4 id="i-com-va-acabar-la-cosa">I com va acabar la cosa?</h4>

<p><strong>M.B.</strong> Em vaig dirigir al DJ que en aquell llavors hi havia al <em>Mile</em>, l’Albert de Síbaris, i li vaig demanar si podia posar la cançó en questió. Ell, per cercar-la em va demanar l’autor. I jo, palplantat sense saber la resposta, vaig demanar-li ajuda al <em>Carra</em>.</p>

<p><strong>M.C.</strong> Inevitablement obligat a intervenir, li vaig dir a l’Albert: “Noche de Sexo”, de Aventura ft. Wisin y Yandel. I la seva resposta va ser: “Aquí hace mucho que no se pone Reggaeton”.</p>

<h4 id="i-no-us-va-donar-cap-més-explicació">I no us va donar cap més explicació?</h4>

<p><strong>M.C.</strong> No ens va donar cap explicació perquè no calia: la seva expressió de mofa no deixava dubtes. De manera que, esperant comprensió, vem explicar als nostres amics el que havia passat. Però malauradament la seva reacció va ser més de indiferència que de suport. Aquí és quan vaig adonar-me’n que calia fer un pas més enllà. Els havia de fer entendre que el <em>Reggaeton</em> no només s’escolta, es viu. Per això li vaig ensenyar al Martí el vídeo de “Ella y yo” de Aventura ft. Don Omar en directe al Madison Square Garden, on els artistes no només canten, sinó que <em>interpreten</em> el seu paper en el triangle amoròs que descriu la cançó.</p>

<p><strong>M.B.</strong> La lletra de “Ella y yo” va ser la primera lletra de <em>Reggaeton</em> a la qual li vaig dedicar atenció. I em vaig emportar una grata sorpresa positiva, ja que va molt més enllà amoroses buides i superficials que tant sovint trobem en la música <em>pop</em>, i hi afegeix un nou element: la passió, encara més observable en l’actuació en directe. I llavors vàrem pensar que si aconseguiem transmetre aquella passió, els nostres amics voldrien tant com nosaltres escoltar <em>Reggaeton</em> en els moments de <em>rauxa</em>.</p>

<h4 id="i-què-veu-pensar-per-a-aconseguir-ho">I què veu pensar per a aconseguir-ho?</h4>

<p><strong>M.C.</strong> Doncs llavors va arribar ja la nit de <em>Halloween</em> (com l’anomenem a Hospitalet) o la <em>Castanyada</em> (com dicta el <em>chauvinisme</em> català). En aquella nit, a alguns dels llavors caps del <em>CAU</em> se’ls va acudir utilitzar les instalacions per a activitats que poc tenen a veure amb l’escoltisme: beure begudes alcohòliques abans d’anar al Milenio. Empassats ja els dos primers <em>cubates</em> vem trobar que era el moment perfecte per posar “Ella y yo”, però un altre cop ens vem trobar que ningú la tenia al mòvil, i ningú tenia internet per a posar YouTube. Aleshores ens vem adonar que ho hauríem de posar tot de la nostra part: l’havíem d’interpretar nosaltres.</p>

<p><strong>M.B.</strong> Jo ja l’havia escoltat prous vegades i em sabia la lletra de memòria. En <em>Carra</em> per suposat feia anys que es va memoritzar fins la última coma. Així que per el to de veu vàrem decidir que jo interpretaria la part del Romeo i en <em>Carra</em> faria de Don Omar. Va ser un gran èxit, gràcies al qual teníem un excèrcit d’amics units a la nostra causa. I aquella mateixa nit tots junts vàrem anar al Milenio a demanar que ens posessin “Ella y yo”.</p>

<h4 id="entenc-doncs-que-aquest-cop-va-haver-hi-sort">Entenc doncs que aquest cop va haver-hi sort?</h4>

<p><strong>M.C.</strong> Tandebò hagués estat tan fàcil. Malgrat que els nostres amics aquella nit van insistir junt amb nosaltres, tant a l’Albert de Síbaris com al Miquel Albert els va ser igual. Van seguir pensant que la vitalitat del beat del Reggaeton els faria mal al seu negoci, com si el <em>Mile</em> tingués cap estàndard a mantenir.</p>

<p><strong>M.B.</strong> Però de totes maneres, aquella nit va acabar sent un punt d’inflexió: els nostres amics havien gravat la nostra interpretació en vídeo, i la van publicar al Facebook. Allà no només vàrem aconseguir el suport del nostre grup més proper: molts altres Santfeliuencs ara volien veure també la performance al Milenio.</p>

<h4 id="llavors-quant-temps-més-va-poder-resistir-la-milenio-a-la-pressió-popular">Llavors, quant temps més va poder resistir la Milenio a la pressió popular?</h4>

<p><strong>M.C.</strong> Relativament poc, però perquè s’hi va ajuntar un altre element determinant. Justament aquell novembre va sortir “Danza Kuduro”, que malgrat ser cantada majoritàriament per Don Omar tenia un <em>beat</em> més lleuger i inevitablement addictiu que va fer que fos acceptada per inclús els més reticents. Estic segur que fins i tot en Miquel Albert l’escoltava en privat. Així que cap al desembre ja es va anar deixant sentir per al Milenio, i amb ella van venir més cançons.</p>

<h4 id="per-com-ho-dius-sembla-que-va-ser-més-aviat-danza-kuduro-el-punt-dinflexió-en-lextensió-del-reggaeton">Per com ho dius sembla que va ser més aviat “Danza Kuduro” el punt d’inflexió en l’extensió del reggaeton…</h4>

<p><strong>M.C.</strong> Cal reconèixer que va ser una ajuda molt important, però si no haguéssim aclimatat prèviament als Santfeliuencs, posaria la mà al foc que no els hagués agradat tampoc “Danza Kuduro”.</p>

<p><strong>M.B.</strong> Però cal contextualitzar encara més: estem parlant de Sant Feliu, que no deixa de formar part del Baix Llobregat. Justament aquell any havíem començat la universitat, i allà els meus companys eren de la Catalunya més vella i pura que no sabia ni situar Sant Feliu de Llobregat al mapa. Jo sempre els deia “Som la capital del Baix Llobregat”, però llavors no entenien com podia tenir vuit cognoms catalans. Això ajuntat amb la meva obsessió al <em>reggaeton</em> els trencava tots els esquemes del que havien cregut que podia ser Catalunya. Però no m’estranya: el vigatà i el gironí mai s’ha dignat a anar més al sud que la Ciutat Comptal. Per ells, qualsevol cosa en <em>la lengua del imperio</em> era considerada una heretgia, de manera que el <em>reggaeton</em> no tenia cap cabuda en la seva imatge platònica de Catalunya.</p>

<p><img src="/assets/images/entrevista_reggaeton/entrevista_reggaeton_02.jpg" alt="Foto #2" title="Fotos de Mireia Planas Sucarrat" /></p>

<blockquote>
  <p>els meus companys d’universitat, originaris de la més pura Catalunya Vella on ningú ha votat mai al PSC no sabien ni situar Sant Feliu al mapa. Jo sempre els deia “Som la capital del Baix Llobregat”, però llavors no entenien com podia tenir vuit cognoms catalans. Això ajuntat amb la meva obsessió al <em>reggaeton</em> els trencava tots els esquemes del que havien cregut que podia ser Catalunya. Però no m’estranya: el vigatà i el gironí mai s’ha dignat a anar més al sud que la Ciutat Comptal.</p>
</blockquote>

<h4 id="i-què-va-caler-per-acostar-a-un-olotí-o-empordanenc-a-la-sub-cultura-de-la-música-llatina">I què va caler per acostar a un Olotí o Empordanenc a la sub-cultura de la música llatina?</h4>

<p><strong>M.B.</strong> Doncs vaig cada nit que fèiem pre-festa al pis d’algú, era molt insistent i els forçava a escoltar cançons senceres. Hi accedien perquè els semblava inevitablement divertit veure com em sabia totes les lletres de memòria. Però la clau va ser que llavors ja es deixaven sentir temes com “Ayer la ví” o “La despedida” a les discoteques Barcelonines, i a part sonaven en els moments crítics en què el cos et demana remembers com Chayanne o Sónia y Selena. I com que ja els havia <em>donat les molles</em> a la pre-festa, llavors i només llavors era quan els meus amics <em>agironats</em> no podien resistir la temptació i <em>volien el pa sencer</em>: <em>el perreo</em>. I vaja, després et demanàven les lletres, i poc a poc se’ls anava desfent el <em>chauvinisme</em> català/gironí que imperava al seu cervell.</p>

<h4 id="un-cop-aconseguit-que-un-estudiant-gironí-escolti-reggaeton-la-resta-és-bufar-i-fer-ampolles-no">Un cop aconseguit que un estudiant gironí escolti <em>reggaeton</em>, la resta és bufar i fer ampolles, no?</h4>

<p><strong>M.B.</strong> En efecte, aquesta va ser la victòria definitiva. El <em>reggaeton</em> va ser un terratrèmol que va trencar definitivament les barreres entre les dues Catalunyes. És l’element cohesiu més important què hi ha hagut mai a Catalunya, l’únic punt comú entre un frequentador de Foro-Coches i els fans de “Els Catarres”. Daddy Yankee i Don Omar pixen damunt del 80è aniversari del PSUC: amb una desena de temes de <em>reggaeton</em> ens han fet <em>un sol poble</em>.</p>

<h4 id="amb-la-rellevància-que-té-tot-el-què-dieu-com-és-que-mai-se-us-ha-reconegut-la-vostra-labor">Amb la rellevància que té tot el què dieu, com és que mai se us ha reconegut la vostra labor?</h4>

<p><strong>M.C.</strong> La cultura catalana està inevitablement polititzada, i als mitjans subvencionats ja els ha costat prou l’acceptació del <em>reggaeton</em> com per a sobre fer-ne un reportatge de com va arribar a Catalunya. Ni els cooperatives independents finançades a través de micromecenatge com el <em>Fet a Sant Feliu</em> s’han dignat a contactar-nos.</p>

<p>Personalment, un dels fets que em van afectar més va ser sentir en primícia “La Calor que em crema” de Trifàssic, la primera cançó de <em>reggaeton</em> en català. Els vaig demanar si podia participar en el seu proper tema i ells em van dir que hi estarien encantats. Encara espero la seva trucada. Des de llavors intento fer la meva carrera en solitari i no conto amb més suport que el dels meus amics</p>

<blockquote>
  <p>La cultura catalana està inevitablement polititzada, i als mitjans subvencionats ja els ha costat prou l’acceptació del <em>reggaeton</em> com per a sobre fer-ne un reportatge de com va arribar a Catalunya. Ni els cooperatives independents finançades a través de micromecenatge com el <em>Fet a Sant Feliu</em> s’han dignat a trucar-nos.</p>
</blockquote>

<p><strong>M.B.</strong> De totes maneres estem orgullosos d’haver aplanat el camí als joves que volen escoltar <em>reggaeton</em> en pubs, discoteques i festes majors. I encara més, també hem ajudat a que joves artistes com <em>Bad Gyal</em> hagin pogut fer-se un nom en el panorama musical català a través d’estils fins fa poc vetats per <em>L’Aranguardia</em> com el <em>trap</em> o <em>reggaeton</em>. No ens calen reconeixements personals. Les nostres motivacions van més enllà. Arribarà un dia en què en les paelles de la Pilar Rahola en Joan Laporta (que tornarà a ser president del Barça) convidarà als més <em>quinquis</em> del filial i junts combinaràn les “Paraules d’amor” d’en Serrat amb el “Llamado de Emergencia” de Daddy Yankee. Per què com deia Miquel Martí Pol, “tot està per fer, i tot és possible”.</p>]]></content><author><name>martibosch</name></author><category term="fait-a-sant-feliu" /><category term="entrevistes" /><category term="musica" /><category term="cultura" /><category term="oci" /><summary type="html"><![CDATA[En Miguel Carrasco, Carra per als amics, és un graduat en Direcció Hotelera, natural de l’Hospitalet de Llobregat. Nascut al 1992, va arribar a Sant Feliu el 2006 ja que feia més d’un any que cursava l’educació secundària a l’Escola Mestral. En Martí Bosch estudià Enginyeria Industrial, i posteriorment un màster en Intel.ligència Artificial. Igual que en Miguel, va néixer l’any de les Olimpíades, però ell va arribar a la Ciutat de Les Roses als 3 anys, per a començar el parvulari al Salvador Espriu.]]></summary></entry></feed>