<?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://freygeospatial.github.io/freygeospatial//feed.xml" rel="self" type="application/atom+xml" /><link href="https://freygeospatial.github.io/freygeospatial//" rel="alternate" type="text/html" /><updated>2026-04-11T13:05:35+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//feed.xml</id><title type="html">[Geo]Data</title><subtitle>A Portfolio and Blog on Software Engineering and Geospatial Data Science</subtitle><author><name>Jordan Frey</name></author><entry><title type="html">Networking Basics: NAT, SSH, and Port Forwarding</title><link href="https://freygeospatial.github.io/freygeospatial//Linux-Networking-Basics/" rel="alternate" type="text/html" title="Networking Basics: NAT, SSH, and Port Forwarding" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Linux-Networking-Basics</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Linux-Networking-Basics/"><![CDATA[<p>I’ve been working through a Linux networking course lately, and wanted to document some of the initial concepts that I found valuable. This post covers NAT, SSH, network interface inspection via the command line, and port forwarding, with a practical focus on getting SSH working inside a virtual machine (VM) environment.</p>

<h3 id="why-this-matters-for-data-engineers">Why This Matters for Data Engineers</h3>

<p>Data engineering work is rarely confined to a single machine. Pipelines pull from remote databases, push to cloud storage, and run on servers accessible only through a terminal. Networking knowledge is therefore empowering, helping you debug issues with these varied services more independently.</p>

<h3 id="nat-network-address-translation">NAT: Network Address Translation</h3>

<p>NAT, or Network Address Translation, is the mechanism that allows devices on a private network to communicate with external networks — and with each other.</p>

<p>In VirtualBox (the open-source hypervisor software allowing me to run VMs), a VM is given a NAT adapter by default. This means the VM can reach the internet through the host machine, and the host can reach the VM through a NAT. What it does <em>not</em> allow by default is VM-to-VM communication. For that, you need to configure a <strong>NAT Network</strong> — a shared virtual network that multiple VMs can join. This can be done through the VirtualBox settings UI.</p>

<h3 id="network-interfaces-ip-a-and-ifconfig">Network Interfaces: ip a and ifconfig</h3>

<p>A network interface is a point of connection between a device and a network. This can be wifi, ethernet, loopback/localhost, etc.</p>

<p>Before setting up SSH or port forwarding, it helps to understand what your network interfaces look like. Two commands are commonly used for this:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ip a</code> — the modern standard on Linux</li>
  <li><code class="language-plaintext highlighter-rouge">ifconfig</code> — the older Unix/macOS equivalent, part of the <code class="language-plaintext highlighter-rouge">net-tools</code> package</li>
</ul>

<p>Both display information about your active network interfaces, including interface names (e.g., <code class="language-plaintext highlighter-rouge">eth0</code>, <code class="language-plaintext highlighter-rouge">lo</code>), IPv4 and IPv6 addresses, MAC addresses, and interface status. They usually show the private ip address of devices on network.</p>

<p><em>Note: if you want to use ifconfig on a Linux machine where it isn’t preinstalled, run <code class="language-plaintext highlighter-rouge">sudo apt install net-tools</code> and then add it to the path. To add it to the path, edit your <code class="language-plaintext highlighter-rouge">~/.bashrc file</code> and append <code class="language-plaintext highlighter-rouge">export PATH="$PATH:/sbin:/usr/sbin"</code> to it. Then, restart your terminal or reload the bashrc file with <code class="language-plaintext highlighter-rouge">source ~/.bashrc</code></em></p>

<h4 id="what-addresses-will-you-see">What addresses will you see?</h4>

<p>The addresses you see fall into a few categories:</p>

<p><strong>Private (RFC 1918) addresses</strong> — routable only within a local network:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">10.0.0.0/8</code></li>
  <li><code class="language-plaintext highlighter-rouge">172.16.0.0/12</code></li>
  <li><code class="language-plaintext highlighter-rouge">192.168.0.0/16</code></li>
</ul>

<p><strong>Other common addresses:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">127.0.0.1</code> / <code class="language-plaintext highlighter-rouge">::1</code> — loopback, also known as localhost. Traffic sent here is intercepted by the OS and looped back to the local machine — it never touches a NIC or a router.</li>
  <li><code class="language-plaintext highlighter-rouge">169.254.x.x</code> — link-local, OR APIPA. <strong>APIPA</strong> is typically assigned when DHCP fails. DHCP is the Dynamic Host Configuration Protocol. It’s the system that automatically assigns IP addresses to devices when they join a network, so you don’t have to set one. APIPA stands for Automatic Private IP Addressing. It’s a fallback mechanism some OS’s use when a device can’t reach a DHCP server. In practice, seeing a 169.254.x.x address is usually a sign that something is wrong — your machine couldn’t find the DHCP server, meaning the router is down, the network cable is unplugged, or there’s some other connectivity issue. Conversely, <strong>link-local</strong> refers to addresses that are only valid on a single network segment — they can’t be routed beyond the directly connected link (cable, Wi-Fi, etc.). <em>Link-local does not necessarily mean that there is a connectivity issue.</em></li>
  <li><code class="language-plaintext highlighter-rouge">fd00::/8</code> or <code class="language-plaintext highlighter-rouge">fe80::/10</code> — IPv6 private and link-local ranges</li>
</ul>

<p>On a typical home or office machine, <code class="language-plaintext highlighter-rouge">ip a</code> will show only private IPs. On a cloud VM (AWS, GCP, etc.), you will often see a private IP on the interface even if the machine has a public IP — the public IP is handled by the cloud provider’s NAT layer and won’t appear in <code class="language-plaintext highlighter-rouge">ip a</code> at all.</p>

<p>To find your <strong>public IP</strong>, query an external service:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl ifconfig.me      <span class="c"># returns whichever IP your request arrived from (may be either IPv4 or IPv6)</span>
curl <span class="nt">-4</span> ifconfig.me   <span class="c"># show IPv4</span>
curl <span class="nt">-6</span> ifconfig.me   <span class="c"># show IPv6</span>
</code></pre></div></div>

<h3 id="ssh-secure-shell">SSH: Secure Shell</h3>

<p>SSH (Secure Shell) lets you control a remote system from the command line over an encrypted connection. If <code class="language-plaintext highlighter-rouge">openssh-server</code> is not already installed on your target machine, you can add it with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>openssh-server
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>ssh
</code></pre></div></div>

<p>The default port for SSH (or other secured connections) on any machine is <strong>22</strong>.</p>

<p>To connect:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh &lt;username&gt;@&lt;ip-address&gt;
</code></pre></div></div>

<p>By default, there is no direct route from the host machine to the VM’s private IP in VirtualBox. Port forwarding is the solution.</p>

<h3 id="port-forwarding">Port Forwarding</h3>

<p>Port forwarding redirects traffic arriving at one address and port to a different address and port. When data arrives at a machine on a specific port, instead of that machine handling it directly, the traffic is forwarded elsewhere.</p>

<h4 id="the-home-router-example">The Home Router Example</h4>

<p>Your router has a single public IP. Behind it are many devices with private IPs, none of which are reachable directly from the internet. Port forwarding solves this:</p>

<ol>
  <li>Someone connects to your public IP on port <code class="language-plaintext highlighter-rouge">8080</code></li>
  <li>Your router sees the incoming traffic and forwards it to a private IP on port <code class="language-plaintext highlighter-rouge">80</code></li>
  <li>The server on that machine responds, and the router sends the reply back</li>
</ol>

<p>The outside world only ever sees the public IP — the internal machine stays hidden.</p>

<p>Other common uses include:</p>
<ul>
  <li>Hosting a game server at home</li>
  <li>SSH-ing into a home machine from outside your network</li>
  <li>Exposing a local web server to the internet</li>
</ul>

<h4 id="port-forwarding-with-virtualbox">Port Forwarding with VirtualBox</h4>

<p>To SSH from your host machine into a VirtualBox VM, configure port forwarding in the VM’s network settings:</p>

<table>
  <thead>
    <tr>
      <th>Setting</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Host IP</td>
      <td><code class="language-plaintext highlighter-rouge">127.0.0.1</code></td>
    </tr>
    <tr>
      <td>Host Port</td>
      <td>Any unused port, e.g. <code class="language-plaintext highlighter-rouge">22022</code></td>
    </tr>
    <tr>
      <td>Guest IP</td>
      <td>The VM’s IP address (from <code class="language-plaintext highlighter-rouge">ip a</code> inside the VM)</td>
    </tr>
    <tr>
      <td>Guest Port</td>
      <td><code class="language-plaintext highlighter-rouge">22</code> (default SSH port)</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 100%; text-align: center;">
        <img src="/images/linux-networking/Screenshot 2026-04-04 at 10.15.58 AM.png" alt="VirtualBox NAT Network configuration" />
        <span style="font-size: 10px">Figure 1</span>
    </div>
</div>

<p>Then SSH into the VM from your host:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh <span class="nt">-p</span> 22022 &lt;username&gt;@127.0.0.1
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-p</code> flag specifies the host port you configured. The traffic hits your localhost on port <code class="language-plaintext highlighter-rouge">22022</code>, gets forwarded by VirtualBox to the VM on port <code class="language-plaintext highlighter-rouge">22</code>, and you have a shell.</p>

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

<p>Networking is one of those topics that feels irrelevant until you find yourself unable to explain why a pipeline can’t reach a database you know is running. The concepts here — NAT, private vs. public IPs, SSH, and port forwarding — come up often enough in data engineering work to make learning these foundations worthwhile, even when they aren’t the focus of what you’re building. I hope this serves as a useful starting point. And as always, happy coding!</p>]]></content><author><name>Jordan Frey</name></author><category term="Linux" /><category term="Networking" /><category term="virtual machines" /><summary type="html"><![CDATA[I’ve been working through a Linux networking course lately, and wanted to document some of the initial concepts that I found valuable. This post covers NAT, SSH, network interface inspection via the command line, and port forwarding, with a practical focus on getting SSH working inside a virtual machine (VM) environment.]]></summary></entry><entry><title type="html">Setup an Ubuntu Virtual Machine on MacOS with the UTM Emulator</title><link href="https://freygeospatial.github.io/freygeospatial//Setup-Ubuntu-VM-with-UTM/" rel="alternate" type="text/html" title="Setup an Ubuntu Virtual Machine on MacOS with the UTM Emulator" /><published>2025-07-20T00:00:00+00:00</published><updated>2025-07-20T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Setup-Ubuntu-VM-with-UTM</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Setup-Ubuntu-VM-with-UTM/"><![CDATA[<p>In all my jobs- previous and current- I’ve been using Linux in one form or another, and I’ve grown to both enjoy and depend on it. For personal projects, I wanted to stick with that environment. My home machine runs macOS, however. To bridge the gap, I turned to UTM — a free, open-source virtualization and emulation tool for macOS. While UTM is a great option and alternative to Parallels, setting up a Linux VM with it does involve a bit of extra effort.</p>

<p>In this post, I’ll walk through how to set up an Ubuntu VM in UTM and configure it so you can SSH into it from your macOS host. If you only need terminal access, SSH is recommended over using the UTM GUI for a smoother and more efficient experience.</p>

<ol>
  <li>Install UTM</li>
  <li>Download your desired Ubuntu instance (use ARM compatible ISO’s for M-series Macs).</li>
  <li>Follow all the steps found here: <a href="https://www.youtube.com/watch?v=1PL-0-5BNXs">https://www.youtube.com/watch?v=1PL-0-5BNXs</a></li>
  <li>Go to the UTM network settings for your new VM</li>
  <li>Change the “Network Mode” to “Emulated VLAN”</li>
  <li>Go to port forwarding and select these options:</li>
</ol>

<div style="display: flex;">
    <div style="flex: 100%; text-align: center;">
        <img src="/images/utm/port-forwarding.png" alt="Port forwarding" />
        <span style="font-size: 10px">Figure 1</span>
    </div>
</div>

<ol>
  <li>Start your Ubuntu instance and log in</li>
  <li>Ensure you can allow for other machines to ssh into the Ubuntu guest machine by running
    <ol>
      <li><code class="language-plaintext highlighter-rouge">sudo apt install openssh-server</code></li>
      <li><code class="language-plaintext highlighter-rouge">sudo systemctl enable ssh</code></li>
    </ol>
  </li>
  <li>In your host terminal, run <code class="language-plaintext highlighter-rouge">ssh -p 22022 &lt;username&gt;@localhost</code></li>
</ol>]]></content><author><name>Jordan Frey</name></author><category term="MacOS" /><category term="Linux" /><category term="virtual machines" /><summary type="html"><![CDATA[In all my jobs- previous and current- I’ve been using Linux in one form or another, and I’ve grown to both enjoy and depend on it. For personal projects, I wanted to stick with that environment. My home machine runs macOS, however. To bridge the gap, I turned to UTM — a free, open-source virtualization and emulation tool for macOS. While UTM is a great option and alternative to Parallels, setting up a Linux VM with it does involve a bit of extra effort.]]></summary></entry><entry><title type="html">The Case for Auto-Formatters and Linters: Elevating Code Quality in Software Engineering</title><link href="https://freygeospatial.github.io/freygeospatial//Style-Guides-and-Coding-Standards/" rel="alternate" type="text/html" title="The Case for Auto-Formatters and Linters: Elevating Code Quality in Software Engineering" /><published>2025-01-04T00:00:00+00:00</published><updated>2025-01-04T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Style-Guides-and-Coding-Standards</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Style-Guides-and-Coding-Standards/"><![CDATA[<p>I will argue here that standardizing the formatting and styling of code on a team is not immaterial to building and deploying high quality software. Though I give examples using Python and GitHub Actions, the logic is applicable for any other language and CI/CD toolset.</p>

<h3 id="ugly-code-reduces-team-efficiency">Ugly Code Reduces Team Efficiency</h3>

<p>Imagine a scenario where your colleague was just asked to enhance some code you built. As part of that work, new modules, classes, and functions must be created that will be reused elsewhere.</p>

<p>You have a different coding style than your coworker though, and while reviewing the <a href="https://en.wikipedia.org/wiki/Diff" target="_blank">diffs</a>, you see a pull request marred with conflicting indentation patterns, varied casing styles, and in-line comments describing the functions inplace of docstrings (or worse, no documentation at all). <em>Hopefully</em> their code is not approved and merged by another engineer before you have a chance to complete your review.</p>

<p>This scenario is a reality on many engineering teams, and is not ~just~ an annoyance for developers that appreciate pretty code; rather, this reality can cause a multitude of issues including:</p>
<ol>
  <li>visual clutter that distracts from the actual code logic being written</li>
  <li>non-standard style practices that cause IDEs to light up with lint warnings everywhere</li>
  <li>reduced efficacy of IDE auto-complete functionality when searching for the correct variable, class, function or method name</li>
  <li>negated functionality of IDEs ability to preview docstring descriptors for modules, classes, functions and methods</li>
  <li>increased time spent deciphering code that could have been spent building features, fixing bugs, or doing (quite literally) anything else.</li>
</ol>

<p>Overall, such discrepancies create a hinderance a team’s productivity and will only become more problematic as projects grow. Ugly code (and undocumented code- thank goodness for <a href="https://peps.python.org/pep-0257/" target="_blank">PEP-257</a>) almost certaintly means longer feature-to-production timetables; it can even lead to bugs from misinterpretted code. Team-leads at a minimum should encourage adaptation of some guideline to adhere to. Preferably, we should attach linters and autoformatters to our pull requests and CI/CD pipelines.</p>

<h3 id="enter-the-black-formatter">…Enter the Black Formatter</h3>
<p>Autoformatters and linters are tools designed to automatically format or check code according to a set of predefined style guidelines or rules; they can also help make code that was written by ten developers and a GPT look like it was written by just one. One of the most popular options for Python is the <a href="https://black.readthedocs.io/en/stable/" target="_blank">Black Formatter</a>, which helps format your Python code according to <a href="https://peps.python.org/pep-0008/" target="_blank">PEP-8</a> industry standards, the most famous of the PEP guidelines.</p>

<p>In the words of its documentation,</p>

<p>      <em>“By using Black, you agree to cede control over minutiae of hand-formatting. In
      return, Black gives you speed, determinism, and freedom from pycodestyle
      nagging about formatting. You will save time and mental energy for more important
      matters.</em></p>

<p>      <em>Black makes code review faster by producing the smallest diffs possible. 
      Blackened code looks the same regardless of the project you’re reading.
      Formatting becomes transparent after a while and you can focus on the content
      instead.”</em></p>

<p>If you use VS Code, you can install the Black Formatter as an extension: <a href="https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter" target="_blank">https://marketplace.visualstudio.com/items?itemName=ms-python.black-formatter</a>.</p>

<p>You can also install black formatter as a CLI utility: <code class="language-plaintext highlighter-rouge">pip install black</code>.</p>

<h3 id="other-useful-linters-and-extensions">Other Useful Linters and Extensions:</h3>
<p>You can use other linters as either an alternative or in addition to Black. Some recommended ones include:</p>
<ul>
  <li>flake8, flakeheaven, flake8-docstrings</li>
  <li>pylint</li>
  <li>ruff</li>
</ul>

<p>Each has their own advantages.</p>

<h3 id="linter-usage-and-enforcement">Linter Usage and Enforcement:</h3>

<p>There are many strategies for how to best enforce linting and formatting on software engineering teams. I will suggest an approach that works best for me, and show how this can be done using CI/CD.</p>

<h4 id="requirements">Requirements:</h4>

<p>To follow this tutorial in its entirety, you must:</p>
<ol>
  <li>Have VS Code installed</li>
  <li>Have Python installed, and a basic understanding of python syntax</li>
  <li>Install these VS Code Extensions:
    <ul>
      <li>Black formatter extension</li>
      <li>Python extension</li>
    </ul>
  </li>
  <li>Have a GitHub account and public repository to hold examples</li>
  <li>A basic understanding of <code class="language-plaintext highlighter-rouge">git</code> concepts, shell scripting, CI/CD, and preferably experience using GitHub Actions or Azure Pipelines</li>
</ol>

<p>It will also be helpful- but not explicitly required- to use these tools locally as you work through this tutorial:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">black</code> formatter CLI utility: <a href="https://github.com/psf/black" target="_blank">https://github.com/psf/black</a></li>
  <li><code class="language-plaintext highlighter-rouge">act</code> utility for running github actions locally: <a href="https://github.com/nektos/act" target="_blank">https://github.com/nektos/act</a></li>
</ul>

<h4 id="workflow">Workflow:</h4>
<p>The easiest methods I have seen has been to first set up your IDE to autoformat your python code automatically upon saving.</p>

<p>After installing the Black Formatter VS Code Extension, I go into my VS Code user settings. On MacOS, these can usually be found at: <code class="language-plaintext highlighter-rouge">/Users/&lt;user&gt;/Library/Application Support/Code/User/settings.json</code>.</p>

<p>Add this to the json:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="nl">"editor.formatOnSave"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="err">,</span><span class="w">
    </span><span class="nl">"[python]"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"editor.formatOnType"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
        </span><span class="nl">"editor.defaultFormatter"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ms-python.black-formatter"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"editor.formatOnSave"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="p">}</span><span class="err">,</span><span class="w">
    </span><span class="nl">"black-formatter.showNotifications"</span><span class="p">:</span><span class="w"> </span><span class="s2">"always"</span><span class="w">
</span></code></pre></div></div>

<p>With these settings, saving a python file will automatically be autoformatted according to Black style guides. If you keep <code class="language-plaintext highlighter-rouge">formatOnSave</code> set to false, you could alternatively right-click the editor, and select “format document” for the formatter to run.</p>

<p>On a team though, it can be hard to enforce contributor IDE settings. A CI/CD rule to check that code is styled according to team guidelines is a necessary second-check.</p>

<p>To properly test that this next piece works as expected, make sure to set the <code class="language-plaintext highlighter-rouge">formatOnSave</code> from the previous step to <code class="language-plaintext highlighter-rouge">false</code>. Then, put two Python files in your project directory (a <code class="language-plaintext highlighter-rouge">src</code> subdirectory is fine, as is the root- just keep it out of the <code class="language-plaintext highlighter-rouge">.github</code> folder). Keep one properly formatted, and the second improperly formatted. You can use these basic examples:</p>

<h5 id="no_issuespy">no_issues.py</h5>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Properly formatted python code:
</span><span class="n">x</span> <span class="o">=</span> <span class="p">{</span><span class="s">"hello"</span><span class="p">:</span> <span class="s">"world"</span><span class="p">}</span>
</code></pre></div></div>

<h5 id="lint_thispy">lint_this.py</h5>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Improperly formatted python code, with added whitespace before the ending curly brace
</span><span class="n">x</span> <span class="o">=</span> <span class="p">{</span><span class="s">"hello"</span><span class="p">:</span> <span class="s">"world"</span> <span class="p">}</span>
</code></pre></div></div>

<p>Then, commit these changes to a branch that is based off of <code class="language-plaintext highlighter-rouge">main</code>, but is not <code class="language-plaintext highlighter-rouge">main</code>.</p>

<p>To check ONLY those Python files that have been modified against the <code class="language-plaintext highlighter-rouge">main</code> branch, first add this shell script to your .github folder under a “scripts” subdirectory:</p>

<h5 id="modified_filessh">modified_files.sh</h5>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># echos modified python files, but excludes those deleted</span>
<span class="nv">changed_files</span><span class="o">=</span><span class="si">$(</span>git diff <span class="nt">--diff-filter</span><span class="o">=</span>d <span class="nt">--name-only</span> <span class="si">$(</span>git merge-base HEAD remotes/origin/main<span class="si">)</span> HEAD | <span class="nb">grep</span> .py<span class="si">)</span>
<span class="nb">echo</span> <span class="k">${</span><span class="nv">changed_files</span><span class="k">}</span>
</code></pre></div></div>

<p>Then, add these steps to your Actions workflow:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">install black formatter</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">pip install black</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Save modified files to environmental variables</span>
        <span class="na">id</span><span class="pi">:</span> <span class="s">get_modified_files</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">echo MODIFIED_FILES=$(./.github/scripts/modified_files.sh) &gt;&gt; $GITHUB_ENV</span>
      
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">return modified python files</span>
        <span class="na">run</span><span class="pi">:</span> <span class="s">echo $(./.github/scripts/modified_files.sh)</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Lint modified Python files with Black</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">if [ -z "$MODIFIED_FILES" ]; then</span>
            <span class="s">echo "No Python files modified."</span>
          <span class="s">else</span>
            <span class="s">black --check --diff --color $MODIFIED_FILES</span>
          <span class="s">fi</span>
</code></pre></div></div>

<p>If there was any improperly formatted Python code, the deployment would fail. You can check this on GitHub Actions in your personal repository. Otherwise, to test this locally, first make sure you have the <code class="language-plaintext highlighter-rouge">nektos/act</code> CLI tool and run the <code class="language-plaintext highlighter-rouge">act</code> command in your terminal, with any options if desired. For example, I usually will run <code class="language-plaintext highlighter-rouge">act &lt;github_event_name&gt; --container-architecture linux/amd64</code>. For all options using this utility, please look into its <a href="https://nektosact.com/" target="_blank">documentation</a>. For my own complete, working example on this workflow, you visit my Github repository: <a href="https://github.com/FreyGeospatial/github_actions" target="_blank">https://github.com/FreyGeospatial/github_actions</a>.</p>

<p>Overall, this is a pretty simple example of what you can do to enforce a cleaner codebase on your team. I encourage you to explore other linters, including <code class="language-plaintext highlighter-rouge">Flake8</code>, or even <code class="language-plaintext highlighter-rouge">ruff</code>, the latter of which has gained popularity recently for being very computationally performant.</p>

<p>I hope you have found this helpful. If you have any comments, questions, or feedback- please reach out! And as always, happy coding!</p>]]></content><author><name>Jordan Frey</name></author><category term="DevOps" /><category term="Python" /><category term="GitHub Actions" /><category term="Shell Scripting" /><summary type="html"><![CDATA[I will argue here that standardizing the formatting and styling of code on a team is not immaterial to building and deploying high quality software. Though I give examples using Python and GitHub Actions, the logic is applicable for any other language and CI/CD toolset.]]></summary></entry><entry><title type="html">Passing Azure Library Values into Docker</title><link href="https://freygeospatial.github.io/freygeospatial//Passing-Azure-Library-Variables-Into-Docker/" rel="alternate" type="text/html" title="Passing Azure Library Values into Docker" /><published>2023-11-09T00:00:00+00:00</published><updated>2023-11-09T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Passing-Azure-Library-Variables-Into-Docker</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Passing-Azure-Library-Variables-Into-Docker/"><![CDATA[<p>I had a project recently where I created a private software package and hosted it on Azure Artifacts, a similar platform to Pypi or Github for distributing packages. There was a scenario where I needed to pass values from Azure Library into Docker.</p>

<p>Since my package was private, it could only be accessed by those with specific credentials- credentials which could authenticate that a user has correct access. One of the services requiring use of this package ran inside a continuously-running AWS ECS task. The ECS task ran from a docker image. In the Dockerfile used to create the image, we have code telling Docker to install some software packages. This is where I would run my <code class="language-plaintext highlighter-rouge">pip install</code> command to pull my package from Azure Artifacts.</p>

<p>Usually when you install a package from Pypi, you simply run <code class="language-plaintext highlighter-rouge">pip install &lt;package name&gt;</code>. If you want to specify a repository other than the default, you can use <code class="language-plaintext highlighter-rouge">pip install &lt;package name&gt; --extra-index-url http://xyz123.com</code></p>

<p>From Azure Artifacts, you need to specificy exactly this, but add credentials to the beginning of the link:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> &lt;package name&gt; <span class="nt">--extra-index-url</span> http://&lt;user&gt;:&lt;token&gt;@xyz123.com
</code></pre></div></div>
<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
         <span style="font-size: 10px">pip install from a private repository</span>
    </div>
</div>

<p>Remember, you can find the link to download your package by clicking the “Connect to Feed” option in Azure Artifacts. (For those unfamiliar, a Feed is a container for packages in Azure DevOps; it provides version and access control for project dependencies).</p>

<p>However, <strong>it is bad practice</strong> (think security flaw) to hardcode credentials directly into the software; it should be passed another way, and it made sense to use Azure’s library feature to store the credentials. Library is a service that is used to store key-value pairs, and only those with proper permissions can access the items. These variables can be passed into any pipeline within the Azure ecosystem. This also makes it possible to work with Azure DevOps even if you want to run the code in another cloud environment like AWS; you would just need to have Azure push the pre-built image to ECR, Amazon’s container repostory service, for instance.</p>

<p>So, we are left with the option to load the credentials into the image at build-time. And don’t worry about the token showing up in the pipeline log messages- the password is obfuscated if set as “secret”:</p>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/azure-library-docker/azure-library-secure-token.png" alt="Secure Azure Library Value" />
         <span style="font-size: 10px">Azure Library</span>
    </div>
</div>

<p>But take note: if you are going to store the image in Azure’s container registry and are only looking for a secure way of downloading packages, this workflow might isn’t <em>totally</em> relevant and might be a little circuitous (but, still good to know and might be tangentially useful). You could simply use a Docker Registry Service Connection to to more directly download packages and run them inside Docker. However, there could be other reasons to pass key-values into Docker. So, Let’s keep on keeping on. In this tutorial, we will simply print out the username stored in the library.</p>

<p>After creating the library group and variable, create your dockerfile. Then, upload it to your Azure repository.</p>

<p>For our purposes, I am skipping any package creation as discussed above. No need to get complex.</p>

<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> python:3.11-slim-bullseye</span>

<span class="k">ARG</span><span class="s"> username # here, we are telling Docker to expect an argument passed during the build process</span>

<span class="k">ENV</span><span class="s"> APP_ENV=${username} # here, we are passing the build argument to an environmental variable.</span>
<span class="k">CMD</span><span class="s"> echo ${APP_ENV} # We are going to print out the variable value to the command line.</span>
</code></pre></div></div>
<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
    <span style="font-size: 10px">DOCKERFILE</span>
    </div>
</div>

<p>In Azure pipelines, we can create a task to build the docker image. There is a specific task to do this, called “Build an Image” in the traditional UI for building azure pipelines. In YAML, the task would be called <code class="language-plaintext highlighter-rouge">Docker@0</code>. You could also use a Bash command instead to accomplish the same thing:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">--build-arg</span> <span class="nv">username</span><span class="o">=</span><span class="si">$(</span>username<span class="si">)</span> <span class="nt">-f</span> <span class="si">$(</span>Build.SourcesDirectory<span class="si">)</span>/Dockerfile
</code></pre></div></div>
<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
    <span style="font-size: 10px">Docker build command from the command line</span>
    </div>
</div>

<p>You might need to change or add additional arguments, including to specify the location of the dockerfile. I put down the default location for this tutorial.</p>

<p>However, I won’t be using the bash command directly in the pipeline, but it is good to know what is happening behind the scenes.</p>

<p>But whatever method you choose to use, you need to connect the pipeline the Azure Library group. In the traditional UI, this is where you can make the connection:</p>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/azure-library-docker/variable-group.png " alt="Connecting to variable group" />
        <span style="font-size: 10px">Connecting to Library Variable Group in traditional pipeline UI</span>
    </div>
</div>

<p><br /></p>

<p>In YAML, you would use a snippet like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">variables</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">Variables-Dev</span>  <span class="c1"># this is where we connect the variable group to the YAML pipeline</span>
</code></pre></div></div>

<p><br /></p>

<p>In the traditional UI, we can  reference the build arguments. We specify the variable by using this format: <code class="language-plaintext highlighter-rouge">$(variable_name)</code>.</p>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/azure-library-docker/BuildPipelineUI.png " alt="Build Pipeline UI" />
        <span style="font-size: 10px">Building and pushing a Docker image in the traditional UI</span>
    </div>
</div>

<p><br /></p>

<p>In YAML, the whole thing would look like this (I’ve removed sensitive information):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">pool</span><span class="pi">:</span>
  <span class="na">vmImage</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>

<span class="na">variables</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">Variables-Dev</span>  <span class="c1"># this is where we connect the variable group to the YAML pipeline</span>

<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">Docker@0</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Build</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">image'</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Azure</span><span class="nv"> </span><span class="s">subscription</span><span class="nv"> </span><span class="s">1</span><span class="nv"> </span><span class="s">(########################)'</span>
    <span class="na">azureContainerRegistry</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{"loginServer":"##############.azurecr.io",</span><span class="nv"> </span><span class="s">"id"</span><span class="nv"> </span><span class="s">:</span><span class="nv"> </span><span class="s">"/subscriptions/################/resourceGroups/##########/providers/Microsoft.ContainerRegistry/registries/##########"}'</span>
    <span class="na">buildArguments</span><span class="pi">:</span> <span class="s1">'</span><span class="s">username=$(username)'</span>
    <span class="na">additionalImageTags</span><span class="pi">:</span> <span class="s1">'</span><span class="s">my_image'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">Docker@0</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Push</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">image'</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Azure</span><span class="nv"> </span><span class="s">subscription</span><span class="nv"> </span><span class="s">1</span><span class="nv"> </span><span class="s">(###############)'</span>
    <span class="na">azureContainerRegistry</span><span class="pi">:</span> <span class="s1">'</span><span class="s">{"loginServer":"################.azurecr.io",</span><span class="nv"> </span><span class="s">"id"</span><span class="nv"> </span><span class="s">:</span><span class="nv"> </span><span class="s">"/subscriptions/##################/resourceGroups/#############/providers/Microsoft.ContainerRegistry/registries/#############"}'</span>
    <span class="na">action</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Push</span><span class="nv"> </span><span class="s">an</span><span class="nv"> </span><span class="s">image'</span>
    <span class="na">additionalImageTags</span><span class="pi">:</span> <span class="s1">'</span><span class="s">my_image'</span>

<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">Docker@2</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">login</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">ACR'</span>
  <span class="na">inputs</span><span class="pi">:</span>
    <span class="na">containerRegistry</span><span class="pi">:</span> <span class="s">testServiceConnection</span>
    <span class="na">command</span><span class="pi">:</span> <span class="s">login</span>

<span class="pi">-</span> <span class="na">script</span><span class="pi">:</span> <span class="s1">'</span><span class="s">docker</span><span class="nv"> </span><span class="s">run</span><span class="nv"> </span><span class="s">################.azurecr.io/testproject:my_image'</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Command</span><span class="nv"> </span><span class="s">Line</span><span class="nv"> </span><span class="s">Script'</span>
</code></pre></div></div>
<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <span style="font-size: 10px">Doing the same thing in a more reproducible and versionable way</span>
    </div>
</div>

<p><br /></p>

<p>And you can see the username populating after running the dockerfile:</p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/azure-library-docker/final_output.png " alt="Final Output" />
        <span style="font-size: 10px">Final output. See how we print the username</span>
    </div>
</div>]]></content><author><name>Jordan Frey</name></author><category term="Azure" /><category term="DevOps" /><summary type="html"><![CDATA[I had a project recently where I created a private software package and hosted it on Azure Artifacts, a similar platform to Pypi or Github for distributing packages. There was a scenario where I needed to pass values from Azure Library into Docker.]]></summary></entry><entry><title type="html">Intersections between Computer Science, Software Engineering, Data Engineering, Spatial Data Science and GIS</title><link href="https://freygeospatial.github.io/freygeospatial//Instersections-between-Data-Science-Engineering-and-GIS/" rel="alternate" type="text/html" title="Intersections between Computer Science, Software Engineering, Data Engineering, Spatial Data Science and GIS" /><published>2023-10-20T00:00:00+00:00</published><updated>2023-10-20T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Instersections-between-Data-Science-Engineering-and-GIS</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Instersections-between-Data-Science-Engineering-and-GIS/"><![CDATA[<p>During and shortly after my undergraduate education, I tried a few different career paths that didn’t work out. Eventually, I felt like GIS could be a way to continue using my environmental degree while pursuing a renewed interest in tech. This was very much true- with some caveats. To be successful in GIS, you should be able to code and have some knowledge in statistics. During my graduate education, I took as many computer science and programming-centric courses I could- and I still felt like there was much, much more to learn. But, it was enough to get me through the door.</p>

<p>I am creating four non-distinct, overlapping areas that I think anyone interested in GIS, spatial data science, and engineering should study- because that is what I did. They are:</p>

<h2 id="computer-science">Computer Science</h2>
<p>Computer science is a broad field, and can be used in a large number of disciplines. It is the study of computers and computational systems. It has many components, but my own experience tells me that its concepts in data structures and algorithms are most fundamental and important. Knowledge of computer science can be <em>extremely</em> beneficial for working with data efficiently.</p>

<h2 id="software-engineering">Software Engineering</h2>
<p>Software engineering is the systematic design, development, testing, and maintenance of software. It is not to be forgotten that software engineering also encompasses project management and quality assurance. It goes well beyond ad hoc scripting- but if you can learn to code, you can learn to be a software engineer. It just takes dedication, like anything else. Knowledge of software engineering can help bring geospatial data and information solutions to your company, the government, and the masses.</p>

<h2 id="data-engineering">Data Engineering</h2>
<p>Data engineering is just a niche within software engineering, focused on data collection and processing. Data engineers build data pipelines- that is, software that helps move data from one place to another. Data pipelines should be built in a reproducible, versioned, automated, scalable, and robust way- minimizing the potential for data loss or data duplication; systems do fail and bugs are inevitably introduced in the software development process, so engineers attempt to mitigate this as much as possible. Usually, data engineers work to consolidate data from a wide variety of sources into a single, standardized data warehouse. The data that is collected is made available to data analysts and scientists for use in creating models, visualizations, dashbaords, and reports. Data engineering is my discipline at the time of writing this post.</p>

<h2 id="spatial-data-science-gis-and-giscience">Spatial Data Science, GIS, and GIScience</h2>
<p>Data science is a multidisciplinary field that combines statistics, artificial intelligence and machine learning, computer science, and industry or scientific knowledge to gain insights from data and predict future outcomes. There can be some overlaps with data engineering when it comes to data collection and munging, but data science specializes in the creation of models to make predictions based on historical data. Often times, the models data scientists create are used as integral parts of software offerings. Other times, the findings are purely academic or ad hoc in nature.</p>

<p>Spatial data science is all of this, but focuses on data with a geographic component, which is an added layer of complexity. Spatial data science is very similar to geographic information <em>science</em> (GIS or GIScience), which is the basis for (and is also semantically similar to) geographic information <em>systems</em> (also GIS). While geographic information <em>systems</em> are the <em>tools</em> primarily concerned with the management, visualization and analysis of geographic data, geographic information <em>science</em> is the <em>theory</em> underpinning geographic information <em>systems</em>. Spatial data science, meanwhile, is more practical in nature than geographic information <em>science</em> by attempting to answer real world questions and often places an emphasis on predictive analyses.</p>

<p>TLDR: you can perform spatial data science using geographic information <em>systems</em>, but you cannot perform spatial data science without knowledge of geographic information <em>science</em>.</p>]]></content><author><name>Jordan Frey</name></author><category term="Data Engineering" /><category term="Data Science" /><category term="GIS" /><summary type="html"><![CDATA[During and shortly after my undergraduate education, I tried a few different career paths that didn’t work out. Eventually, I felt like GIS could be a way to continue using my environmental degree while pursuing a renewed interest in tech. This was very much true- with some caveats. To be successful in GIS, you should be able to code and have some knowledge in statistics. During my graduate education, I took as many computer science and programming-centric courses I could- and I still felt like there was much, much more to learn. But, it was enough to get me through the door.]]></summary></entry><entry><title type="html">Start Using the Power BI REST API with Powershell</title><link href="https://freygeospatial.github.io/freygeospatial//Start-Using-the-Power-BI-REST-API-with-Powershell/" rel="alternate" type="text/html" title="Start Using the Power BI REST API with Powershell" /><published>2021-08-18T00:00:00+00:00</published><updated>2021-08-18T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Start-Using-the-Power-BI-REST-API-with-Powershell</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Start-Using-the-Power-BI-REST-API-with-Powershell/"><![CDATA[<p>Earlier today, I used for the first time the Power BI REST API to access some reports in my company’s workspace. This API can be used with Windows PowerShell. After spending a couple hours familiarizing myself with both PowerShell and the API, I think I’ve finally got the hang of it.</p>

<p>If you’ve never used the Power BI REST API previously with PowerShell previously, you first need to download the module onto your system. Enter the following into the PowerShell console:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># A REST API is simply an interface for interacting with a server. Full details on</span><span class="w">
</span><span class="c"># the API functionality can be found at: https://docs.microsoft.com/en-us/rest/api/power-bi/</span><span class="w">

</span><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">MicrosoftPowerBIMgmt</span><span class="w"> </span><span class="c"># please type this line into your PowerShell console</span><span class="w">
</span></code></pre></div></div>
<p><br /></p>

<p>Next, I will demonstrate how to quickly interact with the API and your published content. If you haven’t yet, open up PowerShell ISE, an IDE for PowerShell Scripting (though, I am sure VS Code would work just fine, too). I encourage you to run each line of code independently (not all at once) and to examine the structure and contents of each variable. And of course, don’t forget to save your script when finished.</p>

<p>Here, you will be able to access metadata on your reports:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Login-Power</span><span class="w"> </span><span class="nx">BI</span><span class="w"> </span><span class="c"># This initial line will open up a login screen for the Power BI service</span><span class="w">

</span><span class="cm">&lt;# 
Ensure you find the group ID for your content. This is located in the URL of your workspace
E.g., app.powerbi.com/groups/&lt;YOUR-GROUP-ID&gt;/list
Also note how in PowerShell, variables are initialized and called using the dollar-sign 
#&gt;</span><span class="w">
</span><span class="nv">$result</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-PowerBIRestMethod</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">URL</span><span class="s2">"https://api.powerbi.com/v1.0/myorg/groups/&lt;YOUR-GROUP-ID&gt;/datasets"</span><span class="w"> </span><span class="nt">-Method</span><span class="w"> </span><span class="nx">Get</span><span class="w">

</span><span class="cm">&lt;# 
Unlike in R where piping is performed with %&gt;% when using the Tidyverse
and |&gt; in base R, PowerShell uses an actual pipe character to transfer the
contents of $result to the function 'ConvertFrom-Json'
#&gt;</span><span class="w">
</span><span class="nv">$workspaceContents</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$result</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w"> 

</span><span class="c"># For testing purposes, let's try to extract the contents of the first report listed.</span><span class="w">
</span><span class="c"># type this into the console and then call $firstWorkspace</span><span class="w">
</span><span class="nv">$firstWorkspace</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$workspaceContents</span><span class="o">.</span><span class="n">value</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="w">

</span><span class="c"># Of course, it's bad practice to specify an object by referencing a direct location.</span><span class="w">
</span><span class="c"># What if that changes? Let's set up the basis of a for-loop by getting the number</span><span class="w">
</span><span class="c"># of reports stored in our workspace:</span><span class="w">
</span><span class="nv">$m</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$workspaceContents</span><span class="o">.</span><span class="nf">value</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">measure</span><span class="w">

</span><span class="cm">&lt;# 
Now we can create our for-loop. Note the similarities of a PowerShell 
for-loop structure with Java syntax. '-lt' is a less-than operator here,
specifying to iterate i (beginning at 0) with increments of 1 until it 
reaches less than the value of $m.Count
#&gt;</span><span class="w">
</span><span class="kr">for</span><span class="w"> </span><span class="p">(</span><span class="nv">$i</span><span class="o">=</span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="nv">$m</span><span class="o">.</span><span class="nf">Count</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="o">++</span><span class="p">){</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$workspaceContents</span><span class="o">.</span><span class="n">value</span><span class="p">[</span><span class="nv">$i</span><span class="p">]</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s2">"My Geospatial Report"</span><span class="p">){</span><span class="w"> </span><span class="c"># '-eq' is the equals operator</span><span class="w">
        </span><span class="nv">$desired_report</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$workspaceContents</span><span class="o">.</span><span class="n">value</span><span class="p">[</span><span class="nv">$i</span><span class="p">]</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"The report was found!"</span><span class="w"> </span><span class="c"># this is a print-to-console operation</span><span class="w">
        </span><span class="kr">break</span><span class="w"> </span><span class="c"># this stops the loop at the current iteration</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># call the report metadata:</span><span class="w">
</span><span class="nv">$desired_report</span><span class="w">
</span></code></pre></div></div>

<p>I may post more tutorials and code snippets on Power BI API implementations in the future. Until then, I would strongly recommend watching <a href="https://www.youtube.com/@GuyInACube" target="_blank">Guy in a Cube</a> tutorials on YouTube. The channel has some great content for all things Power BI, including implementation of the REST API. Thanks for stopping by, and happy coding!</p>]]></content><author><name>Jordan Frey</name></author><category term="Power BI" /><category term="Powershell" /><category term="Business Intelligence" /><summary type="html"><![CDATA[Earlier today, I used for the first time the Power BI REST API to access some reports in my company’s workspace. This API can be used with Windows PowerShell. After spending a couple hours familiarizing myself with both PowerShell and the API, I think I’ve finally got the hang of it.]]></summary></entry><entry><title type="html">How to Connect an R Shiny Dashboard to AWS S3</title><link href="https://freygeospatial.github.io/freygeospatial//Connecting-R-Shny-To-AWS/" rel="alternate" type="text/html" title="How to Connect an R Shiny Dashboard to AWS S3" /><published>2021-08-08T00:00:00+00:00</published><updated>2021-08-08T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Connecting-R-Shny-To-AWS</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Connecting-R-Shny-To-AWS/"><![CDATA[<p>The applications for hosting an R Shiny web application on the cloud are huge, and learning to work with cloud services in general is critical for data analysts, engineers, and scientists. Dashboards that rely on cloud-hosted data do not need to be constantly redeployed; as long they point to the right file on S3 (or Redshift or RDS database), your Shiny application will use the most recently available data. This has the potential to reduce overhead and provide near-real time access to information for your clients.</p>

<p>In this post, I will demonstrate how to:
1) Set up an Amazon Web Services (AWS) S3 Bucket
3) Connect S3 to an R script
4) Use this connection in an R Shiny dashboard.</p>

<h2 id="setting-up-an-amazon-s3-bucket">Setting up an Amazon S3 Bucket</h2>

<p><strong>Create an AWS Account</strong>. You will be asked for an email, password, and credit card information. As long as you stay within Free Tier restrictions, you will not be charged. Only my use of the AWS Key Management Service (KMS) has incurred charges from the workflow described in this tutorial, but currently I’ve only been charged $0.12.</p>

<p><strong>Create an S3 Bucket</strong>. The name for your bucket must be unique- no other AWS user must have created a bucket with the same name.</p>

<p><strong>Choose an applicable region</strong>. The “cloud” just refers to data centers that Amazon maintains.  These data centers are located in various regions. I chose the region closest to me as of the writing of this tutorial. Ensure that you remember the region you picked; transferring data between regions can incur costs, so if you have other AWS services that you anticipate needing in the future, having them located in one region will reduce spending.</p>

<div style="display: flex;">
    <div style="flex: 80%; text-align: center;">
        <img src="/images/r-shiny-aws/createbucket_orig.png" alt="create s3 bucket" />
    </div>
</div>

<p><br /></p>

<p><strong>Block all public access to this bucket</strong>. Amazon recommends this security feature be activated to prevent unrestricted access to the contents of your bucket.</p>

<div style="display: flex;">
    <div style="flex: 80%; text-align: center;">
        <img src="/images/r-shiny-aws/blockaccess_orig.png" alt="create keys" />
    </div>
</div>

<p><br /></p>

<p><strong>Encrypt your data</strong>. I can’t stress the importance of data security enough. I recommend you enable server-side encryption. This will encrypt all the data at the object level that enters the bucket. You have the option of using either SSE-S3 encryption of SSE-KMS (Key Management Service) encryption. It is up to you, however, to decide what is more suitable for your needs. I personally chose to use SSE-KMS, with symmetric encryption. I then assigned a specific IAM user to have access to the key. (For details on setting up an IAM user, see the next step; this can be done concurrently).</p>

<p>When you use SSE-KMS, you create an <em>Access Key ID</em>, <em>Secret Access Key</em>, and <em>user</em> for said key. Ensure you keep these credentials safe- you will need these for APIs and other services (e.g, R Shiny).</p>

<div style="display: flex;">
    <div style="flex: 80%; text-align: center;">
        <img src="/images/r-shiny-aws/kms_orig.png" alt="create keys" />
    </div>
</div>

<p><br /></p>

<p><b>Set-up IAM users:</b> IAM stands for Identity and Access Management. This allows you to enable users to access certain AWS services without needing access to the root account. This improves security and reduces risk of unauthorized access to data and cloud services.</p>

<p>​<b>Complete the set-up of your bucket and upload files.</b> Feel free to upload some test files. I decided to upload an RDS file for ease of use with R. This RDS file contains a simple Leaflet map I created previously that I wanted to be able to showcase easily on Shinyapps.io. I ensured all my read/write permissions for the file were set to ‘private’.</p>

<p><b>​Optional: Set up budget alerts.</b> In the management console under billing, you have the option to set up email and SNS alerts if you begin to go over budget. I have mine set to $10/month, and will receive notifications if I approach 80% of that amount.</p>

<h3 id="connect-r-to-your-s3-bucket">Connect R to your S3 Bucket</h3>

<p>Now comes the easy part! Seriously- connecting R to S3 is a piece of cake, if you are already an R programmer. First, store your S3 credentials in a file named “Renviron” in your working directory. <em>While it is not good practice to hard code credentials</em>, this should get your code up and running for at least a proof of concept and demo. <b>Just don’t forget to pass these credentials in using a safer method before putting it into production!</b></p>

<p>The format for saving your credentials in the Renviron file should be as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AWS_ACCESS_KEY_ID = "XXXXXXXXXXX"
AWS_SECRET_ACCESS_KEY = "XXXXXXXXX"
AWS_DEFAULT_REGION = "XXXXXXX"
</code></pre></div></div>

<p>R will know where to find these credentials. I’ve also hidden the file names and paths within text files located in my working directory for the same reason. Run my code below (albeit with the correct folder paths):</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">library</span><span class="p">(</span><span class="n">aws.s3</span><span class="p">)</span><span class="w">

</span><span class="n">s3BucketName</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">scan</span><span class="p">(</span><span class="s2">"bucketname.txt"</span><span class="p">,</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"txt"</span><span class="p">)</span><span class="w">
</span><span class="n">s3File</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">scan</span><span class="p">(</span><span class="s2">"filepath.txt"</span><span class="p">,</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"txt"</span><span class="p">)</span><span class="w">

</span><span class="c1"># These files are located on the bucket</span><span class="w">
</span><span class="n">file_names</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">get_bucket_df</span><span class="p">(</span><span class="n">s3BucketName</span><span class="p">)</span><span class="w">

</span><span class="c1"># This loads my desired file - in my case, a Leaflet map stored in an RDS file.</span><span class="w">
</span><span class="n">myMap</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">s3readRDS</span><span class="p">(</span><span class="n">object</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">file_names</span><span class="p">[</span><span class="n">file_names</span><span class="o">$</span><span class="n">Key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">s3File</span><span class="p">,</span><span class="w"> </span><span class="s2">"Key"</span><span class="p">],</span><span class="w"> </span><span class="n">bucket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s3BucketName</span><span class="p">)</span><span class="w">

</span><span class="n">myMap</span><span class="w">
</span></code></pre></div></div>

<p>Implementing this into a ​Shiny dashboard is relatively straightforward once you’ve completed the previous task. We simply need to wrap all this into a new script called app.R.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">library</span><span class="p">(</span><span class="n">shiny</span><span class="p">)</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">shinydashboard</span><span class="p">)</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">leaflet</span><span class="p">)</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">aws.s3</span><span class="p">)</span><span class="w">

</span><span class="n">s3BucketName</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">scan</span><span class="p">(</span><span class="s2">"bucketname.txt"</span><span class="p">,</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"txt"</span><span class="p">)</span><span class="w">
</span><span class="n">s3File</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">scan</span><span class="p">(</span><span class="s2">"filepath.txt"</span><span class="p">,</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"txt"</span><span class="p">)</span><span class="w">

</span><span class="c1"># files in bucket</span><span class="w">
</span><span class="n">file_names</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">get_bucket_df</span><span class="p">(</span><span class="n">s3BucketName</span><span class="p">)</span><span class="w">
</span><span class="n">myMap</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">s3readRDS</span><span class="p">(</span><span class="n">object</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">file_names</span><span class="p">[</span><span class="n">file_names</span><span class="o">$</span><span class="n">Key</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">s3File</span><span class="p">,</span><span class="w"> </span><span class="s2">"Key"</span><span class="p">],</span><span class="w"> </span><span class="n">bucket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">s3BucketName</span><span class="p">)</span><span class="w">

</span><span class="n">ui</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">dashboardPage</span><span class="p">(</span><span class="w">
    </span><span class="n">dashboardHeader</span><span class="p">(),</span><span class="w">
    </span><span class="n">dashboardSidebar</span><span class="p">(),</span><span class="w">
    </span><span class="n">dashboardBody</span><span class="p">(</span><span class="w">
        </span><span class="n">leafletOutput</span><span class="p">(</span><span class="s2">"mymap"</span><span class="p">,</span><span class="w"> </span><span class="n">height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"92vh"</span><span class="p">)</span><span class="w"> </span><span class="c1">#this text is an ID that must match `output$var_name` below***</span><span class="w">
    </span><span class="p">)</span><span class="w">
</span><span class="p">)</span><span class="w">

</span><span class="n">server</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">input</span><span class="p">,</span><span class="w"> </span><span class="n">output</span><span class="p">,</span><span class="w"> </span><span class="n">session</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    
    </span><span class="n">output</span><span class="o">$</span><span class="n">mymap</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">renderLeaflet</span><span class="p">({</span><span class="w"> </span><span class="c1">#****</span><span class="w">
        </span><span class="n">myMap</span><span class="w">
    </span><span class="p">})</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">shinyApp</span><span class="p">(</span><span class="n">ui</span><span class="p">,</span><span class="w"> </span><span class="n">server</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<p>This is a pretty bare-bones dashboard- merely a structure for you to build off of. But I hope you’ve found this tutorial helpful for merging these technologies together. And once you build out the the dashboard, deploying it online is pretty simple using <a href="Shinyapps.io" target="_blank">Shinyapps.io</a>.</p>

<p>And again, don’t forget that after your demo and POC, you will want to pull in your AWS credentials from a secrets store, whether that is with GitHub Secrets, AWS Parameter Store, or AWS Secrets Manager.</p>

<p>If you decide to host your application with AWS EC2 to host the application, you might decide to use <a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html" target="_blank">instance profiles</a> as a method of authenticating your application against your cloud provider.</p>

<p>Thanks for reading, and as always, happy coding!</p>]]></content><author><name>Jordan Frey</name></author><category term="R" /><category term="Business Intelligence" /><category term="AWS" /><summary type="html"><![CDATA[The applications for hosting an R Shiny web application on the cloud are huge, and learning to work with cloud services in general is critical for data analysts, engineers, and scientists. Dashboards that rely on cloud-hosted data do not need to be constantly redeployed; as long they point to the right file on S3 (or Redshift or RDS database), your Shiny application will use the most recently available data. This has the potential to reduce overhead and provide near-real time access to information for your clients.]]></summary></entry><entry><title type="html">Geospatial Time Series Analysis: Monitoring Trends in PM2.5 in NYC Using R</title><link href="https://freygeospatial.github.io/freygeospatial//Monitoring-Trends-In-PM25-R/" rel="alternate" type="text/html" title="Geospatial Time Series Analysis: Monitoring Trends in PM2.5 in NYC Using R" /><published>2020-05-01T00:00:00+00:00</published><updated>2020-05-01T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Monitoring-Trends-In-PM25-R</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Monitoring-Trends-In-PM25-R/"><![CDATA[<p>This collaborative project between myself and <a href="https://github.com/verma-priyanka">Priyanka Verma</a> seeks to gain insights from particulate matter air pollutant trends in the New York Metropolitan Area. Specifically, we look at particulate matter that is less than 2.5 micrometers (PM2.5), and only data obtained during winter months, due to the higher level of PM2.5 during that season.  We developed this in a way that should be easy to reproduce and understand.</p>

<p><br /></p>

<h3 id="study-area">Study Area</h3>
<p>Our study area includes part of the New-York Metropolitan Area, including New York City, Long Island, counties in Upstate New York, and large portions of New Jersey. These areas were determined by the New York Core Based Statistical Area (NY CBSA).</p>

<p><br /></p>

<h3 id="data">Data</h3>
<p>PM2.5 data has been provided by the Environmental Protection Agency (EPA), and can be found through the open data portal: <a href="https://www.epa.gov/outdoor-air-quality-data/download-daily-data">https://www.epa.gov/outdoor-air-quality-data/download-daily-data</a>.</p>

<p>The CBSA boundary shapefile can be obtained from the United States Census Bureau.</p>

<p><br /></p>

<h3 id="software-packages-and-libraries">Software Packages and Libraries</h3>
<p>To begin the analysis, first load the packages we will be using for this project. Please type in install.packages(“PACKAGE_NAME_HERE”) for each package you do not already have locally installed (all
those used in this project are listed below). Then, run the code chunk below:</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">library</span><span class="p">(</span><span class="n">sf</span><span class="p">)</span><span class="w"> </span><span class="c1"># offers an exceptional data structure for storing GIS vector data </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">tidyverse</span><span class="p">)</span><span class="w"> </span><span class="c1"># compilation of packages curated to make data cleaning easy </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">raster</span><span class="p">)</span><span class="w"> </span><span class="c1"># data structure for storing raster data and preprocessing functions </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">gstat</span><span class="p">)</span><span class="w"> </span><span class="c1"># package for interpolating data</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">lubridate</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides functionality for working with dates and times </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">plyr</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides extra data cleaning functions</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">tmap</span><span class="p">)</span><span class="w"> </span><span class="c1"># interactive visualization functionality for geospatial data </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">rasterVis</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides enhanced plotting functions for raster data </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">RColorBrewer</span><span class="p">)</span><span class="w"> </span><span class="c1"># enhanced and customized palettes for visualizing data </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">reshape</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides extra functionality for data cleaning </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">Kendall</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides acccess to Mann-Kendall modeling </span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">EcoGenetics</span><span class="p">)</span><span class="w"> </span><span class="c1"># provides access to Theil-Sen modeling</span><span class="w">
</span><span class="n">library</span><span class="p">(</span><span class="n">imager</span><span class="p">)</span><span class="w"> </span><span class="c1"># easy plotting of non-spatial image files, like .BMP or .PNG</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="data-pre-processing">Data Pre-Processing</h3>
<p>The raw data used in this analysis spans a 10-year period, from January 1st, 2010 to April 12th, 2020. Air quality data was sampled at each location at least once every few days, making it a near-daily data set. To focus on the yearly winter season, the data was filtered based on the December - March months. The mean PM2.5 value was obtained for each time index (season). Users of this tutorial are encouraged to test view the output of each intermediate step to better understand how the workflow operates.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Reads all csv files in this directory</span><span class="w">
</span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">list.files</span><span class="p">(</span><span class="n">path</span><span class="o">=</span><span class="s2">"data"</span><span class="p">,</span><span class="w"> </span><span class="n">pattern</span><span class="o">=</span><span class="s2">"*.csv"</span><span class="p">,</span><span class="w"> </span><span class="n">full.names</span><span class="o">=</span><span class="kc">TRUE</span><span class="p">)</span><span class="w">
</span><span class="c1"># Merges all csv files into a single dataframe for analysis</span><span class="w">
</span><span class="n">data_all</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">ldply</span><span class="p">(</span><span class="n">data</span><span class="p">,</span><span class="w"> </span><span class="n">read_csv</span><span class="p">)</span><span class="w">
</span><span class="c1"># replace spaces in column names with '_'</span><span class="w">
</span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">gsub</span><span class="p">(</span><span class="s2">" "</span><span class="p">,</span><span class="w"> </span><span class="s2">"_"</span><span class="p">,</span><span class="w"> </span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">))</span><span class="w">
</span><span class="c1"># rename latitude &amp; longitude columns</span><span class="w">
</span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">)[</span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"SITE_LONGITUDE"</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="s2">"x"</span><span class="w">
</span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">)[</span><span class="nf">names</span><span class="p">(</span><span class="n">data_all</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"SITE_LATITUDE"</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="s2">"y"</span><span class="w">
</span><span class="c1"># Convert to date format for filtering</span><span class="w">
</span><span class="n">data_all</span><span class="o">$</span><span class="n">Date</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">data_all</span><span class="o">$</span><span class="n">Date</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> </span><span class="n">as.Date</span><span class="p">(</span><span class="s2">"%m/%d/%Y"</span><span class="p">)</span><span class="w">
</span><span class="c1"># extract year from Date as a new column for yearly interpolation</span><span class="w">
</span><span class="n">data_all</span><span class="p">[,</span><span class="w"> </span><span class="s2">"year"</span><span class="p">]</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">format</span><span class="p">(</span><span class="n">data_all</span><span class="p">[,</span><span class="s2">"Date"</span><span class="p">],</span><span class="w"> </span><span class="s2">"%Y"</span><span class="p">)</span><span class="w">
</span><span class="c1"># filter for winter months</span><span class="w">
</span><span class="n">df_winter</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">data_all</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> 
  </span><span class="n">filter</span><span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="n">data_all</span><span class="o">$</span><span class="n">Date</span><span class="p">,</span><span class="w"> </span><span class="s2">"%m"</span><span class="p">)</span><span class="w"> </span><span class="o">%in%</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s1">'12'</span><span class="p">,</span><span class="s1">'01'</span><span class="p">,</span><span class="s1">'02'</span><span class="p">,</span><span class="s1">'03'</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="spatial-conversion">Spatial Conversion</h3>
<p>The shapefile for the CBSA is first loaded into the R working environment. Then, sampling locations were extracted from the .csv file of PM2.5, and converted into a spatial object.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># read in cbsa shapefile</span><span class="w">
</span><span class="n">nycbsa</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">st_read</span><span class="p">(</span><span class="s2">"data/ny_cbsa.shp"</span><span class="p">)</span><span class="w">

</span><span class="c1"># convert sites to an sf object using projection from cbsa</span><span class="w">
</span><span class="n">epa_sites</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">df_winter</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> 
  </span><span class="n">distinct</span><span class="p">(</span><span class="n">Site_ID</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> 
  </span><span class="n">st_as_sf</span><span class="p">(</span><span class="n">coords</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s2">"x"</span><span class="p">,</span><span class="w"> </span><span class="s2">"y"</span><span class="p">),</span><span class="w"> </span><span class="n">crs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">st_crs</span><span class="p">(</span><span class="n">nycbsa</span><span class="p">))</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="explore-metropolitan-area--air-pollution-monitoring-sites">Explore Metropolitan Area &amp; Air Pollution Monitoring Sites</h3>
<p>This chunk of code visualizes the sampling locations and the overarching area of interest. Monitoring site that falls outside the CBSA boundary will be removed from the analysis.</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tmap_mode</span><span class="p">(</span><span class="s2">"view"</span><span class="p">)</span><span class="w">

</span><span class="n">tm_shape</span><span class="p">(</span><span class="n">nycbsa</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"NY Metropolitan Area"</span><span class="p">,</span><span class="w"> </span><span class="n">is.master</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w"> 
  </span><span class="n">tm_borders</span><span class="p">(</span><span class="n">col</span><span class="o">=</span><span class="w"> </span><span class="s2">"coral"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
  </span><span class="n">tm_layout</span><span class="p">(</span><span class="n">title</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"New York Metro Area Air Pollution Monitoring Sites"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
  </span><span class="n">tm_shape</span><span class="p">(</span><span class="n">epa_sites</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"EPA Air Monitoring Sites"</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
  </span><span class="n">tm_dots</span><span class="p">(</span><span class="n">col</span><span class="o">=</span><span class="s2">"red"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>Let’s take a look at our inputs:</p>

<p><img src="/images/pm25/monitoring_sites.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<h3 id="rasterize">Rasterize</h3>
<p>The NYCBSA data object is currently in the form of a vector file, but needs to be converted into a raster image to create an interpolation surface. This code chunk uses the <code class="language-plaintext highlighter-rouge">rasterize</code> function from the <code class="language-plaintext highlighter-rouge">raster</code> library for this conversion, and then plots our area of interest.</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># create a raster file with resolution of 0.01 for ny metro area</span><span class="w">
</span><span class="n">target</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">raster</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">extent</span><span class="p">(</span><span class="n">nycbsa</span><span class="p">),</span><span class="w"> </span><span class="n">crs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">crs</span><span class="p">(</span><span class="n">nycbsa</span><span class="p">),</span><span class="w"> </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">0.01</span><span class="p">)</span><span class="w">
</span><span class="c1"># assign value of 1 to each pixel</span><span class="w">
</span><span class="n">values</span><span class="p">(</span><span class="n">target</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="m">1</span><span class="w">
</span><span class="c1"># crop raster surface to NY metro region</span><span class="w">
</span><span class="n">nycbsar</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">nycbsa</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> </span><span class="n">rasterize</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">.</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">target</span><span class="p">,</span><span class="w"> </span><span class="n">field</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"GEOID"</span><span class="p">)</span><span class="w">
</span><span class="c1"># plot for visual check</span><span class="w">
</span><span class="n">par</span><span class="p">(</span><span class="n">mar</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">0</span><span class="p">,</span><span class="w"> </span><span class="m">4</span><span class="p">))</span><span class="w">
</span><span class="n">plot</span><span class="p">(</span><span class="n">nycbsar</span><span class="p">,</span><span class="w"> </span><span class="n">axes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">,</span><span class="w"> </span><span class="n">box</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">,</span><span class="w"> </span><span class="n">legend</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">FALSE</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p><img src="/images/pm25/raster.png" alt="Raster" width="375" height="300" /></p>

<p><br /></p>

<h3 id="interpolation">Interpolation</h3>
<p>There needs to be a few more preprocessing steps to ready the dataset for analysis, and then use the <code class="language-plaintext highlighter-rouge">gstat</code> function to perform an inverse distance weighting (IDW) interpolation.</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># assign year to winter months</span><span class="w">
</span><span class="c1"># if jan-march, assign to previous year, else assign same year</span><span class="w">
</span><span class="n">df_winter</span><span class="o">$</span><span class="n">winter</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">ifelse</span><span class="p">(</span><span class="n">format</span><span class="p">(</span><span class="n">df_winter</span><span class="o">$</span><span class="n">Date</span><span class="p">,</span><span class="w"> </span><span class="s2">"%m"</span><span class="p">)</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="s2">"12"</span><span class="p">,</span><span class="w">
                           </span><span class="n">df_winter</span><span class="o">$</span><span class="n">year</span><span class="p">,</span><span class="nf">as.numeric</span><span class="p">(</span><span class="n">df_winter</span><span class="o">$</span><span class="n">year</span><span class="p">)</span><span class="m">-1</span><span class="p">)</span><span class="w">
</span><span class="c1"># create a list of all winter years</span><span class="w">
</span><span class="n">years</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">unique</span><span class="p">(</span><span class="n">df_winter</span><span class="o">$</span><span class="n">winter</span><span class="p">)</span><span class="w">

</span><span class="c1"># Interpolate for each winter season</span><span class="w">
</span><span class="c1"># Calculate mean for every site for each winter season</span><span class="w">
</span><span class="c1"># int_df.list: List of dataframes with mean PM2.5 for each winter</span><span class="w">
</span><span class="c1"># invdist.list List of interpolated surfaces for each winter</span><span class="w">
</span><span class="n">int_df.list</span><span class="w"> </span><span class="o">=</span><span class="nf">list</span><span class="p">()</span><span class="w">
</span><span class="n">invdist.list</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">list</span><span class="p">()</span><span class="w">
</span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">each_year</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="n">years</span><span class="p">){</span><span class="w">
  </span><span class="c1"># calculate mean for every site for each winter season</span><span class="w">
  </span><span class="n">int_df</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">df_winter</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> 
  </span><span class="n">filter</span><span class="p">(</span><span class="n">winter</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="n">each_year</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w"> 
  </span><span class="n">group_by</span><span class="p">(</span><span class="n">Site_ID</span><span class="p">,</span><span class="w"> </span><span class="n">winter</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">,</span><span class="w"> </span><span class="n">y</span><span class="p">)</span><span class="w"> </span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">summarise_at</span><span class="p">(</span><span class="n">vars</span><span class="p">(</span><span class="n">Daily_Mean_PM2.5_Concentration</span><span class="p">),</span><span class="w">
               </span><span class="nf">list</span><span class="p">(</span><span class="n">PM_Mean</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">mean</span><span class="p">))</span><span class="w">
  </span><span class="c1"># add dataframe to list</span><span class="w">
  </span><span class="n">int_df.list</span><span class="p">[[</span><span class="n">each_year</span><span class="p">]]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">int_df</span><span class="w">
  
  </span><span class="c1"># inverse distance interpolation for each year</span><span class="w">
  </span><span class="c1"># create a gstat object</span><span class="w">
  </span><span class="n">invdist</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">gstat</span><span class="p">(</span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"PM_Mean"</span><span class="p">,</span><span class="w">
                   </span><span class="n">formula</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">PM_Mean</span><span class="w"> </span><span class="o">~</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w">
                   </span><span class="n">locations</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">~</span><span class="n">x</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">y</span><span class="p">,</span><span class="w">
                   </span><span class="n">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">int_df</span><span class="p">)</span><span class="w">
  </span><span class="c1"># generate interpolated layer</span><span class="w">
  </span><span class="n">invdistr</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">interpolate</span><span class="p">(</span><span class="n">object</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nycbsar</span><span class="p">,</span><span class="w"> </span><span class="n">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">invdist</span><span class="p">)</span><span class="w">
  </span><span class="c1"># mask to metro area</span><span class="w">
  </span><span class="n">invdistrmsk</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">mask</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">invdistr</span><span class="p">,</span><span class="w"> </span><span class="n">mask</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">nycbsa</span><span class="p">)</span><span class="w">
  </span><span class="c1"># add interpolated layer to list</span><span class="w">
  </span><span class="n">invdist.list</span><span class="p">[[</span><span class="n">each_year</span><span class="p">]]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">invdistrmsk</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="plotting-the-interpolated-layer">Plotting the Interpolated Layer</h3>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># create a stack for all interpolated layers for plotting</span><span class="w">
</span><span class="n">s</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">raster</span><span class="o">::</span><span class="n">stack</span><span class="p">(</span><span class="n">invdist.list</span><span class="p">)</span><span class="w">
</span><span class="c1"># edit name for each layer</span><span class="w">
</span><span class="nf">names</span><span class="p">(</span><span class="n">s</span><span class="p">)</span><span class="w"> </span><span class="o">&lt;-</span><span class="w">  </span><span class="n">gsub</span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">names</span><span class="p">(</span><span class="n">s</span><span class="p">),</span><span class="w"> </span><span class="n">pattern</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"X"</span><span class="p">,</span><span class="w"> </span><span class="n">replacement</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Winter"</span><span class="p">)</span><span class="w">
</span><span class="c1"># set up theme for plot</span><span class="w">
</span><span class="n">mapTheme</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">rasterTheme</span><span class="p">(</span><span class="n">region</span><span class="o">=</span><span class="n">brewer.pal</span><span class="p">(</span><span class="m">8</span><span class="p">,</span><span class="s2">"Blues"</span><span class="p">),</span><span class="w"> </span><span class="n">rev</span><span class="o">=</span><span class="kc">TRUE</span><span class="p">)</span><span class="w">
</span><span class="c1"># plot interpolated layers</span><span class="w">
</span><span class="n">levelplot</span><span class="p">(</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="n">pretty</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">,</span><span class="w">  </span><span class="n">main</span><span class="o">=</span><span class="s2">"NY Metropolitan Area PM2.5 Mean"</span><span class="p">,</span><span class="w"> </span><span class="n">par.settings</span><span class="o">=</span><span class="n">mapTheme</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="saving-interpolated-raster-images-as-rst">Saving Interpolated Raster Images as .RST</h3>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="m">1</span><span class="o">:</span><span class="nf">length</span><span class="p">(</span><span class="n">s</span><span class="o">@</span><span class="n">layers</span><span class="p">)){</span><span class="w">
  </span><span class="n">writeRaster</span><span class="p">(</span><span class="n">s</span><span class="p">[[</span><span class="n">i</span><span class="p">]],</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">paste0</span><span class="p">(</span><span class="s2">"terrset/"</span><span class="p">,</span><span class="w"> </span><span class="nf">names</span><span class="p">(</span><span class="n">s</span><span class="p">[[</span><span class="n">i</span><span class="p">]]),</span><span class="w"> </span><span class="s2">".rst"</span><span class="p">),</span><span class="w">
              </span><span class="n">overwrite</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">F</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><img src="/images/pm25/mean.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<h3 id="histogram">Histogram</h3>

<p>Histograms are a great way to view how PM2.5 measurements change year-to-year within the study area. Each bar represents a sampling location and the amount of PM2.5 measured there. You can see some sampling locations with increasing PM2.5 values over the 10-year time span, while others have decreasing PM2.5 values.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># raster stack to dataframe for each x, y</span><span class="w">
</span><span class="c1"># remove NA values</span><span class="w">
</span><span class="n">stack_df</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">as.data.frame</span><span class="p">(</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="n">xy</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">,</span><span class="w"> </span><span class="n">na.rm</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="kc">TRUE</span><span class="p">)</span><span class="o">%&gt;%</span><span class="w">
  </span><span class="n">melt</span><span class="p">(</span><span class="n">id.vars</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="s1">'x'</span><span class="p">,</span><span class="s1">'y'</span><span class="p">))</span><span class="w">

</span><span class="c1"># histogram for interpolated layers</span><span class="w">
</span><span class="n">par</span><span class="p">(</span><span class="n">mfrow</span><span class="o">=</span><span class="nf">c</span><span class="p">(</span><span class="m">4</span><span class="p">,</span><span class="m">2</span><span class="p">))</span><span class="w">
</span><span class="n">ggplot</span><span class="p">(</span><span class="n">stack_df</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
  </span><span class="n">geom_histogram</span><span class="p">(</span><span class="n">aes</span><span class="p">(</span><span class="n">value</span><span class="p">),</span><span class="w"> </span><span class="n">binwidth</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">.5</span><span class="p">)</span><span class="w"> </span><span class="o">+</span><span class="w">
    </span><span class="n">facet_wrap</span><span class="p">(</span><span class="o">~</span><span class="n">variable</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><img src="/images/pm25/hist.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<h3 id="mann-kendall-trend-test-tau--p-value">Mann-Kendall Trend Test: Tau &amp; P-Value</h3>
<p>A Mann-Kendall model is a non-parametric test similar to a pearson correlation analysis. Ranging from +1 to -1, a positive tau value indicates an increasing trend while a negative tau value indicates a decreasing trend. The higher the absolute tau value, the more consistent that trend is. This helps us answer the question, “where are PM2.5 measurements falling, and where are they rising?”</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># function for Mann-Kendall trend test</span><span class="w">
</span><span class="n">kendall</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">function</span><span class="p">(</span><span class="n">x</span><span class="p">){</span><span class="nf">return</span><span class="p">(</span><span class="n">unlist</span><span class="p">(</span><span class="n">MannKendall</span><span class="p">(</span><span class="n">x</span><span class="p">)))}</span><span class="w">
</span><span class="c1"># apply function to raster stack. RUN THIS CHUNK IN CONSOLE ONLY</span><span class="w">
</span><span class="c1"># takes too long in markdown and ignore "WARNING: Error exit, tauk2. IFAULT =  </span><span class="w">
</span><span class="c1"># 10" warning. Output has been validated in TerrSet, and this message appears </span><span class="w">
</span><span class="c1"># to be a bug in the package. For simplicity, an R object (.rds file) has been </span><span class="w">
</span><span class="c1"># saved to the working directory, which contains the output for the Mann-Kendall</span><span class="w">
</span><span class="n">kendall_output</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">calc</span><span class="p">(</span><span class="n">s</span><span class="p">,</span><span class="n">kendall</span><span class="p">)</span><span class="w">

</span><span class="c1"># Save output to a .rds file</span><span class="w">
</span><span class="c1">#saveRDS(kendall_output, file = "kendall_output.rds")</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<p>Because of some bugs running the above code chunk in an R-Markdown file, we recommend running the above chunk in the R console specifically. If you continue to have trouble running the above code (e.g, the code infinitely runs without stopping after a few minutes), we have provided the output for you as a .rds file which can be easily read into RStudio.</p>

<p>These file types are useful in R; they can be read in directly into your global environment using the same data structure it was created in, without running any further conversions or data processing steps, saving time and preventing potential bugs in your code. To save this file, we have have used the <code class="language-plaintext highlighter-rouge">saveRDS()</code> function seen above. If you are familiar with Python programming, <code class="language-plaintext highlighter-rouge">saveRDS()</code> is analogous to <code class="language-plaintext highlighter-rouge">pickle.dump()</code> from the <code class="language-plaintext highlighter-rouge">pickle</code> Python package.</p>

<p>In any respect, we recommend trying to run the code from the above chunk first. If you have trouble using that code, you can instead load in the provided file using this code here:</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">kendall_output</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">readRDS</span><span class="p">(</span><span class="s2">"output/kendall_output.rds"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="plotting-mann-kendall-output-and-saving-to-rst">Plotting Mann-Kendall Output and saving to .RST</h3>

<p>The <code class="language-plaintext highlighter-rouge">levelplot()</code> function for raster images can be considered to be an upgrade to the generic <code class="language-plaintext highlighter-rouge">plot()</code> function. It provides enhanced functionality and better visualizations.</p>

<p>Notice that the <code class="language-plaintext highlighter-rouge">kendall_output</code> data structure is that of a RasterBrick. RasterBricks are similar to RasterStacks. In both, you can store multiple raster images within the object, allowing for easier file organization and analysis. To access raster images within a brick, type the name of the object, followed by a dollar sign and the name of the raster you are searching for.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># plot Kendall's tau statistic</span><span class="w">
</span><span class="n">levelplot</span><span class="p">(</span><span class="n">kendall_output</span><span class="o">$</span><span class="n">tau</span><span class="p">,</span><span class="w"> </span><span class="n">main</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Kendall's tau statistic"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p><img src="/images/pm25/kendall_tau.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># plot p-value</span><span class="w">
</span><span class="n">levelplot</span><span class="p">(</span><span class="n">kendall_output</span><span class="o">$</span><span class="n">sl</span><span class="p">,</span><span class="w"> </span><span class="n">main</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"p-value"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><img src="/images/pm25/kendall_pval.png" alt="Monitoring Sites" width="375" height="300" /></p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># write to rst</span><span class="w">
</span><span class="n">writeRaster</span><span class="p">(</span><span class="n">kendall_output</span><span class="o">$</span><span class="n">tau</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"output/tau.rst"</span><span class="p">,</span><span class="w"> </span><span class="n">overwrite</span><span class="o">=</span><span class="kc">TRUE</span><span class="p">)</span><span class="w">
</span><span class="n">writeRaster</span><span class="p">(</span><span class="n">kendall_output</span><span class="o">$</span><span class="n">sl</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="s2">"output/mk_p-value.rst"</span><span class="p">,</span><span class="w"> </span><span class="n">overwrite</span><span class="o">=</span><span class="kc">TRUE</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p>You may notice from these plots that at approximately 40.5N, 74.7W PM2.5 tends to increase consistently over the 10-year time frame, while other area see more consistently downward trends. Remember, however, that these estimates are very dependent on the interpolation method used. IDW was chosen for its simplicity for this tutorial, though other methods may yield more accurate results.</p>

<p><br /></p>

<h3 id="terrset-validation-kendall-tau--p-value">TerrSet Validation: Kendall Tau &amp; P-Value</h3>
<p>To ensure R is returning the values we expect (and that the package was not poorly written, or that there was some methodological flaw), it is good to compare results with those in software you may be more familiar with, such as TerrSet. After performing a Mann-Kendall test in the TerrSet Earth Trends Modeler, we have found the results between these two programs to be virtually identical.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ktau_img</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">load.image</span><span class="p">(</span><span class="s2">"terrset/TERRSET_MK_TAU.BMP"</span><span class="p">)</span><span class="w">
</span><span class="n">plot</span><span class="p">(</span><span class="n">ktau_img</span><span class="p">,</span><span class="w"> </span><span class="n">axes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">F</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p><img src="/images/pm25/TERRSET_MK_TAU.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mkp_img</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">load.image</span><span class="p">(</span><span class="s2">"terrset/TERRSET_MK_P.BMP"</span><span class="p">)</span><span class="w">
</span><span class="n">plot</span><span class="p">(</span><span class="n">mkp_img</span><span class="p">,</span><span class="w"> </span><span class="n">axes</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">F</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><img src="/images/pm25/TERRSET_MK_P.png" alt="Monitoring Sites" width="375" height="300" /></p>

<p><br /></p>

<h2 id="theil-sen-median-trend-test">Theil-Sen Median Trend Test</h2>
<p>Knowing simply whether trends are increasing or decreasing is not necessarily enough. It is also useful to know the matgnitude, or rate at which trends are increasing. The Theil-Sen median trend analysis returns a slope for each pixel, which can tell us rates of change in our time series.</p>

<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># saves two images automatically to working directory: </span><span class="w">
</span><span class="c1"># pvalue, and slope of theil-sen.</span><span class="w">
</span><span class="c1"># Note that this function takes at least 10 minutes of processing time </span><span class="w">
</span><span class="c1"># to complete, so be prepared to wait.</span><span class="w">
</span><span class="n">eco.theilsen</span><span class="p">(</span><span class="n">s</span><span class="p">,</span><span class="w"> </span><span class="n">dates</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">c</span><span class="p">(</span><span class="n">seq</span><span class="p">(</span><span class="n">from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2009</span><span class="p">,</span><span class="w"> </span><span class="n">by</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">1</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="m">2019</span><span class="p">)))</span><span class="w">
</span><span class="n">file.rename</span><span class="p">(</span><span class="n">from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"pvalue.tif"</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"theilsen_pvalue.tif"</span><span class="p">)</span><span class="w">
</span><span class="n">file.rename</span><span class="p">(</span><span class="n">from</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"slope.tif"</span><span class="p">,</span><span class="w"> </span><span class="n">to</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"theilsen_slope.tif"</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<p>A Theil-Sen median trend image shows us the slope, or rate of change in a time series analysis. You can see that there is a positive rate of change located at approximately 40.5N, 74.7W, with negative rates of change elsewhere.</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">theilsen_slope</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">raster</span><span class="p">(</span><span class="s2">"output/theilsen_slope.tif"</span><span class="p">)</span><span class="w">
</span><span class="n">plot</span><span class="p">(</span><span class="n">theilsen_slope</span><span class="p">,</span><span class="w"> </span><span class="n">main</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"output/Theil-Sen Median Slope"</span><span class="p">)</span><span class="w">

</span><span class="c1"># write slope image to IDRISI raster format for easy comparison</span><span class="w">
</span><span class="n">writeRaster</span><span class="p">(</span><span class="n">theilsen_slope</span><span class="p">,</span><span class="w"> </span><span class="n">filename</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"output/theilsen_slope.rst"</span><span class="p">,</span><span class="w"> </span><span class="n">overwrite</span><span class="o">=</span><span class="nb">T</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p><br /></p>

<h3 id="terrset-validation-theil-sen-median-trend">TerrSet Validation: Theil-Sen Median Trend</h3>
<p>Similarly, we can validate our results in TerrSet, and we found the results from these two programs to be identical.</p>
<div class="language-R highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">theilsen_slope_img</span><span class="w"> </span><span class="o">&lt;-</span><span class="w"> </span><span class="n">load.image</span><span class="p">(</span><span class="s2">"terrset/TERRSET_THEILSEN_SLOPE.BMP"</span><span class="p">)</span><span class="w">
</span><span class="n">plot</span><span class="p">(</span><span class="n">theilsen_slope_img</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>
<p><img src="/images/pm25/TERRSET_THEILSEN_SLOPE.png" alt="Monitoring Sites" width="375" height="300" /></p>]]></content><author><name>Jordan Frey</name></author><category term="R" /><category term="GIS" /><category term="Data Science" /><summary type="html"><![CDATA[This collaborative project between myself and Priyanka Verma seeks to gain insights from particulate matter air pollutant trends in the New York Metropolitan Area. Specifically, we look at particulate matter that is less than 2.5 micrometers (PM2.5), and only data obtained during winter months, due to the higher level of PM2.5 during that season. We developed this in a way that should be easy to reproduce and understand.]]></summary></entry><entry><title type="html">Streamlining a GIS Workflow with PostGIS</title><link href="https://freygeospatial.github.io/freygeospatial//Streamlining-a-GIS-Workflow-with-PostGIS/" rel="alternate" type="text/html" title="Streamlining a GIS Workflow with PostGIS" /><published>2020-02-10T00:00:00+00:00</published><updated>2020-02-10T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Streamlining-a-GIS-Workflow-with-PostGIS</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Streamlining-a-GIS-Workflow-with-PostGIS/"><![CDATA[<p>Last fall semester, I was able to take an independent study where I expanded my knowledge of SQL beyond its traditional use of querying relational databases for text and numeric values. Instead, I began using it as a standalone GIS, using it to store and query geometries (points, lines, and polygons).</p>

<p>One of the ways in which I initially learned to use PostGIS functions (my new RDMS of choice) was to redo some homework assignments from a previous introductory course in GIS- but using SQL functions instead of ArcMap geoprocessing tools. This particular homework assignment sought to illustrate a use case for buffering, dissolving, and intersecting. By transliterating the workflow from ArcMap into SQL code, I was able to create a process that was reproducible, easy to understand, and more efficient to run. Plus, there was no file bloat of intermediary files- a common data management problem  when geoprocessing data in ArcGIS.</p>

<p>The assignment simulates a problem easy to envision occurring in the natural resources sector: “Forest management staff have asked you to develop information to assist with the application of pesticides… Management intends to apply pesticides to areas within 200 meters of each infested point.” The goals of the project include:</p>
<ul>
  <li>Finding total area of each forest cover type within 200 meters of an infested point</li>
  <li>Finding out whether streams pass through the application areas</li>
  <li>Finding the length of each stream segment that passes through an application area, and what type of forest cover it passes through</li>
</ul>

<p><em>*You can download the data for this tutorial on my github, <a href="https://github.com/FreyGeospatial/freygeospatial.github.io/tree/master/_data" target="_blank">here</a>.</em></p>

<p>We are given 4 different set of geometries, stored as shapefiles:</p>
<ul>
  <li>Park boundary (polygon)</li>
  <li>Infestation points (points)</li>
  <li>Streams (lines)</li>
  <li>Forest cover type (polygon)</li>
</ul>

<p>First, I uploaded each shapefile into its own table in the database with a specified schema. I used <code class="language-plaintext highlighter-rouge">ogr2ogr</code> through the command prompt to accomplish this.</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ogr2ogr <span class="nt">-f</span> <span class="s2">"PostgreSQL"</span> PG:<span class="s2">"host=localhost user=postgres dbname=postgres password=******* port=5432"</span> D:<span class="se">\F</span>ILE_PATH<span class="se">\b</span>oundary.shp <span class="nt">-overwrite</span> <span class="nt">-lco</span> <span class="nv">precision</span><span class="o">=</span>NO <span class="nt">-lco</span> <span class="nv">GEOMETRY_NAME</span><span class="o">=</span>geom <span class="nt">-nln</span> <span class="s2">"staging.kemo_bound"</span>
ogr2ogr <span class="nt">-f</span> <span class="s2">"PostgreSQL"</span> PG:<span class="s2">"host=localhost user=postgres dbname=postgres password=******* port=5432"</span> D:<span class="se">\F</span>ILE_PATH<span class="se">\i</span>nfest_point.shp <span class="nt">-overwrite</span> <span class="nt">-lco</span> <span class="nv">precision</span><span class="o">=</span>NO <span class="nt">-lco</span> <span class="nv">GEOMETRY_NAME</span><span class="o">=</span>geom <span class="nt">-nln</span> <span class="s2">"staging.kemo_infest_points"</span>
ogr2ogr <span class="nt">-f</span> <span class="s2">"PostgreSQL"</span> PG:<span class="s2">"host=localhost user=postgres dbname=postgres password=******* port=5432"</span> D:<span class="se">\F</span>ILE_PATH<span class="se">\s</span>treams.shp <span class="nt">-overwrite</span> <span class="nt">-lco</span> <span class="nv">precision</span><span class="o">=</span>NO <span class="nt">-lco</span> <span class="nv">GEOMETRY_NAME</span><span class="o">=</span>geom <span class="nt">-nln</span> <span class="s2">"staging.kemo_streams"</span>
ogr2ogr <span class="nt">-f</span> <span class="s2">"PostgreSQL"</span> PG:<span class="s2">"host=localhost user=postgres dbname=postgres password=******* port=5432"</span> D:<span class="se">\F</span>ILE_PATH<span class="se">\v</span>egetation.shp <span class="nt">-overwrite</span> <span class="nt">-lco</span> <span class="nv">precision</span><span class="o">=</span>NO <span class="nt">-lco</span> <span class="nv">GEOMETRY_NAME</span><span class="o">=</span>geom <span class="nt">-nln</span> <span class="s2">"staging.kemo_vegetation"</span>
</code></pre></div></div>
<p><em>*<code class="language-plaintext highlighter-rouge">ogr2ogr</code> comes with <a href="https://gdal.org/" target="_blank">GDAL</a>. Install that if you have not already.</em></p>

<p><br /></p>

<p>Let’s take a look at our inputs:</p>
<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/boundary.PNG" alt="Points" />
        <div style="font-weight: bold;">Boundary</div>
    </div>
    <div style="flex: 26.5%; text-align: center;">
        <img src="/images/postgis-intro/points.PNG" alt="Points" />
        <div style="font-weight: bold;">Points</div>
    </div>
    <div style="flex: 24.5%; text-align: center;">
        <img src="/images/postgis-intro/streams.PNG" alt="Streams" />
        <div style="font-weight: bold;">Streams</div>
    </div>
    <div style="flex: 26%; text-align: center;">
        <img src="/images/postgis-intro/vegetation.PNG" alt="Vegetation" />
        <div style="font-weight: bold;">Vegetation</div>
    </div>
</div>

<p><br /></p>

<p>The first task was to create a buffer around the infestation points, and to dissolve the boundaries. You can do this in a couple ways. The first is the create a nested query, like so:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">--create buffers and dissolve boundaries</span>
<span class="k">select</span> <span class="p">(</span><span class="n">st_dump</span><span class="p">(</span><span class="n">st_union</span><span class="p">(</span><span class="n">blah</span><span class="p">.</span><span class="n">st_buffer</span><span class="p">))).</span><span class="n">geom</span> <span class="k">from</span>
<span class="p">(</span><span class="k">select</span> <span class="n">st_buffer</span><span class="p">(</span><span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span>
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span><span class="p">)</span> <span class="k">as</span> <span class="n">blah</span>
</code></pre></div></div>

<p><br /></p>

<p>You can think of this method as querying the results of another query. Using nested queries when I first learned database management helped me understand more intuitively how databases functioned. Additionally, it was crucial for helping me to understand the WITH statement (also called a common table expression, or CTE), which is very important for making large queries easier to read and understand. Nested queries by themselves can often be messy, lack conciseness, and can become overly verbose. Though important to learn, it is often best to avoid using them if possible. A better method would be the following:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="p">(</span><span class="n">st_dump</span><span class="p">(</span><span class="n">st_union</span><span class="p">(</span><span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)))).</span><span class="n">geom</span> 
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">;</span>
</code></pre></div></div>
<p><br />
Both result in the same output. See below:</p>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/buffer_dissolve.PNG " alt="Points" />
        <div style="font-weight: bold;">Dissolved Buffer</div>
    </div>
    <div style="flex: 26.5%; text-align: center;">
        <img src="/images/postgis-intro/records.PNG" alt="records" />
    </div>
</div>

<p>Let’s break down this process. First, we use the <code class="language-plaintext highlighter-rouge">ST_Buffer</code> function, on <code class="language-plaintext highlighter-rouge">a.geom</code> (with ‘a’ being the table, infest_points, as it has been given an alias), and give it a buffer of 200 meters. <code class="language-plaintext highlighter-rouge">Geom</code> is the column that holds the geometry of <code class="language-plaintext highlighter-rouge">infest_points</code>. Remember that we set this name originally in our <code class="language-plaintext highlighter-rouge">ogr2ogr</code> command. Using <code class="language-plaintext highlighter-rouge">ST_Buffer</code> alone (without the other functions) would produce the following result:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span>
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">;</span>
</code></pre></div></div>
<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/buffer_only.PNG " alt="buffer only" />
        <div style="font-weight: bold;">Buffer Only</div>
    </div>
</div>

<p><br /></p>

<p>As you can see, we still need a dissolve. Which leads us to adding the function, <code class="language-plaintext highlighter-rouge">st_union()</code>.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">st_union</span><span class="p">(</span><span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">))</span>
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">;</span>
</code></pre></div></div>
<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/union_alone.PNG " alt="Union Only" />
        <div style="font-weight: bold;">Single Record from Union and Buffer Only</div>
    </div>
</div>

<p>The problem with this, though, is that no longer maintain the individuality of each buffered point. Each buffer has now been merged into a single record, which is not what we want. To avoid this, we use <code class="language-plaintext highlighter-rouge">ST_Dump()</code>. According to the PostGIS documentation, <code class="language-plaintext highlighter-rouge">ST_Dump</code> can be thought of as the opposite of a <code class="language-plaintext highlighter-rouge">GROUP BY</code> clause. Remember to add <code class="language-plaintext highlighter-rouge">.geom</code> at the end of your <code class="language-plaintext highlighter-rouge">select</code> statement, otherwise you will be returned an improperly formatted column formatted geometry column without the desired visuals.</p>

<p>Knowing this general methodology, let’s skip ahead to my near-final product. You will now find that at this stage in my coding, I reverted back to nested loops! Not the most pleasant sight. It’s hard to believe, but this hard-to-read chunk identifies the forest cover within each buffered location, and those sections that are within the park boundary. But how does it work? What are those inner joins doing? Not easy to tell with the way this code is formatted…</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">select</span> <span class="n">veg_intersect_t</span><span class="p">.</span><span class="n">ogc_fid</span><span class="p">,</span> <span class="n">veg_intersect_t</span><span class="p">.</span><span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">st_intersection</span><span class="p">,</span> <span class="n">geom</span><span class="p">)</span> <span class="k">from</span>
<span class="p">(</span><span class="k">select</span> <span class="n">ogc_fid</span><span class="p">,</span> <span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">dissolve_geom</span><span class="p">,</span> <span class="n">geom</span><span class="p">)</span> <span class="k">FROM</span> <span class="c1">--select FID, vegetation class, and only geometry that is within buffer zone</span>
<span class="p">(</span><span class="k">SELECT</span> <span class="n">dissolve</span><span class="p">.</span><span class="n">dissolve_geom</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="o">*</span> <span class="k">FROM</span>
<span class="p">(</span><span class="k">SELECT</span> <span class="p">(</span><span class="n">st_dump</span><span class="p">(</span><span class="n">st_union</span><span class="p">(</span><span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)))).</span><span class="n">geom</span> <span class="k">as</span> <span class="n">dissolve_geom</span> <span class="c1">--buffer and dissolve points</span>
<span class="k">FROM</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">)</span> <span class="k">AS</span> <span class="n">dissolve</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_vegetation</span> <span class="k">as</span> <span class="n">b</span> <span class="k">ON</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">dissolve</span><span class="p">.</span><span class="n">dissolve_geom</span><span class="p">,</span> <span class="n">b</span><span class="p">.</span><span class="n">geom</span><span class="p">))</span> <span class="k">as</span> <span class="n">joined</span><span class="p">)</span> <span class="k">as</span> <span class="n">veg_intersect_t</span>
<span class="k">INNER</span> <span class="k">JOIN</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_bound</span> <span class="k">as</span> <span class="k">c</span> <span class="k">on</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">veg_intersect_t</span><span class="p">.</span><span class="n">st_intersection</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span>
</code></pre></div></div>
<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/veg_in_buffer.PNG" alt="Points" />
        <div style="font-weight: bold;">Vegetation within Buffered Zones</div>
    </div>
    <div style="flex: 26.5%; text-align: center;">
        <img src="/images/postgis-intro/veg_in_buffer_records.PNG" alt="Records" />
    </div>
</div>

<p><br /></p>

<p>What I am about to show will start to make the above more readable. I will introduce the <code class="language-plaintext highlighter-rouge">WITH</code> statement, or CTE query, which as I mentioned previously provides an easier way to write large SQL queries. See the documentation for that <a href="https://www.postgresql.org/docs/9.1/queries-with.html" target="_blank">here</a>. This code chunk below is equivalent to the above, and a bit easier on the eyes:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">buffer</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="p">(</span><span class="n">st_dump</span><span class="p">(</span><span class="n">st_union</span><span class="p">(</span><span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)))).</span><span class="n">geom</span>
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">),</span>

<span class="n">vegetation_intersect</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="n">ogc_fid</span><span class="p">,</span> <span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="n">buffer</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span> 
<span class="k">from</span> <span class="n">buffer</span>
<span class="k">inner</span> <span class="k">join</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_vegetation</span> <span class="k">as</span> <span class="n">b</span> <span class="k">on</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="n">buffer</span><span class="p">.</span><span class="n">geom</span><span class="p">))</span>

<span class="k">select</span> <span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">ogc_fid</span><span class="p">,</span> <span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">st_intersection</span><span class="p">,</span> <span class="n">blah</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span> <span class="k">as</span> <span class="n">good_geom</span>
<span class="k">from</span> <span class="n">vegetation_intersect</span>
<span class="k">inner</span> <span class="k">join</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_bound</span> <span class="k">as</span> <span class="n">blah</span> <span class="k">on</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">st_intersection</span><span class="p">,</span> <span class="n">blah</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span>
</code></pre></div></div>

<p><br /></p>

<p>The WITH statement is akin to a nested query, but allows users to separate their code into chunks, piping one query into the next quite easily, even on an ad hoc basis. See how we can easily add an extra line at the end of the above code to return the area of each forest type that will be impacted by the pesticide application:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">buffer</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="p">(</span><span class="n">st_dump</span><span class="p">(</span><span class="n">st_union</span><span class="p">(</span><span class="n">st_buffer</span><span class="p">(</span><span class="n">a</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="mi">200</span><span class="p">)))).</span><span class="n">geom</span>
<span class="k">from</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_infest_points</span> <span class="k">as</span> <span class="n">a</span><span class="p">),</span>

<span class="n">vegetation_intersect</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="n">ogc_fid</span><span class="p">,</span> <span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="n">buffer</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span> 
<span class="k">from</span> <span class="n">buffer</span>
<span class="k">inner</span> <span class="k">join</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_vegetation</span> <span class="k">as</span> <span class="n">b</span> <span class="k">on</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">b</span><span class="p">.</span><span class="n">geom</span><span class="p">,</span> <span class="n">buffer</span><span class="p">.</span><span class="n">geom</span><span class="p">)),</span>

<span class="n">hygge</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">ogc_fid</span><span class="p">,</span> <span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">class2</span><span class="p">,</span> <span class="n">st_intersection</span><span class="p">(</span><span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">st_intersection</span><span class="p">,</span> <span class="n">blah</span><span class="p">.</span><span class="n">geom</span><span class="p">)</span> <span class="k">as</span> <span class="n">good_geom</span>
<span class="k">from</span> <span class="n">vegetation_intersect</span>
<span class="k">inner</span> <span class="k">join</span> <span class="n">staging</span><span class="p">.</span><span class="n">kemo_bound</span> <span class="k">as</span> <span class="n">blah</span> <span class="k">on</span> <span class="n">st_intersects</span><span class="p">(</span><span class="n">vegetation_intersect</span><span class="p">.</span><span class="n">st_intersection</span><span class="p">,</span> <span class="n">blah</span><span class="p">.</span><span class="n">geom</span><span class="p">)),</span>

<span class="n">woohoo</span> <span class="k">as</span> <span class="p">(</span><span class="k">select</span> <span class="o">*</span><span class="p">,</span> <span class="n">st_area</span><span class="p">(</span><span class="n">hygge</span><span class="p">.</span><span class="n">good_geom</span><span class="p">)</span> <span class="k">from</span> <span class="n">hygge</span><span class="p">)</span>

<span class="k">select</span> <span class="n">class2</span><span class="p">,</span> <span class="k">sum</span><span class="p">(</span><span class="n">st_area</span><span class="p">)</span> <span class="o">*</span> <span class="n">random</span><span class="p">()</span>
<span class="k">from</span> <span class="n">woohoo</span>
<span class="k">group</span> <span class="k">by</span> <span class="n">class2</span>
</code></pre></div></div>

<p><br /></p>

<div style="display: flex;">
    <div style="flex: 30%; text-align: center;">
        <img src="/images/postgis-intro/random.PNG " alt="records" />
    </div>
</div>

<p><em>*Note that I added the <code class="language-plaintext highlighter-rouge">random()</code> function to the code just as an assurance that no future student will copy my results.</em></p>

<p><br /></p>

<p>The <code class="language-plaintext highlighter-rouge">WITH</code> statement turns a convoluted chunk of code into something easily reproducible, and makes streamlining the workflow a relatively quick process. It is probably one of the most important concepts I have learned this past semester.</p>

<p>I may write a future blog to investigate in more detail how intersections using <code class="language-plaintext highlighter-rouge">ST_Intersects</code> and <code class="language-plaintext highlighter-rouge">INNER JOIN</code> work in PostGIS. That tutorial will also seek to answer those two remaining questions: <em>Do any streams pass through the application areas? What is the length of each stream segment that passes through an application area?</em></p>]]></content><author><name>Jordan Frey</name></author><category term="Data Engineering" /><category term="Postgres/PostGIS" /><category term="SQL" /><category term="GIS" /><summary type="html"><![CDATA[Last fall semester, I was able to take an independent study where I expanded my knowledge of SQL beyond its traditional use of querying relational databases for text and numeric values. Instead, I began using it as a standalone GIS, using it to store and query geometries (points, lines, and polygons).]]></summary></entry><entry><title type="html">Hosting a PostGIS Database on AWS EC2</title><link href="https://freygeospatial.github.io/freygeospatial//Hosting-PostGIS-on-EC2/" rel="alternate" type="text/html" title="Hosting a PostGIS Database on AWS EC2" /><published>2019-12-12T00:00:00+00:00</published><updated>2019-12-12T00:00:00+00:00</updated><id>https://freygeospatial.github.io/freygeospatial//Hosting-PostGIS-on-EC2</id><content type="html" xml:base="https://freygeospatial.github.io/freygeospatial//Hosting-PostGIS-on-EC2/"><![CDATA[<p>This semester, my peers <a href="https://www.linkedin.com/in/aaron-eubank-53065b111/" target="_blank">Aaron Eubank</a>, <a href="https://www.linkedin.com/in/bryce-stouffer-07240716b/" target="_blank">Bryce Stouffer</a>, <a href="https://www.linkedin.com/in/samuelwatson2/" target="_blank">Samuel Watson</a>, <a href="https://www.linkedin.com/in/weihongloh/" target="_blank">Wei Hong Loh</a>, and I took on a directed study under our professor <a href="https://www.linkedin.com/in/lyndonestes/" target="_blank">Dr. Lyndon Estes</a> on the implementation and use of PostGIS. While I had prior experience with SQL Server and some of its spatial functions, I had never used SQL to perform a full range of geoprocessing tasks. I found my workflows to be more reproducible than using ArcGIS and QGIS, and found processing speeds to be faster than R’s <code class="language-plaintext highlighter-rouge">{sf}</code> package.</p>

<p>Our professor noted to us, however, that working on a locally hosted database has become outmoded in the face of cloud computing. He recommended that we learn to set up PostGIS on AWS and learning how to secure it against unauthorized access. I had never used AWS prior to this semester, and didn’t know too much about what was involved. As it turns out, use of these cloud services often necessitates knowledge of the terminal/command prompt and its associated functions, which I had little prior experience with. By the end of the semester, though, I became much more proficient in its use and how to improve data security.</p>

<h2 id="the-project">The Project</h2>

<p>We set out to host geosptial data on a PostgreSQL database. To visualize the data, we used Leaflet.js in combination with GeoServer. We found that GeoServer improved interoperability between our front and backend solutions, and even added an additional layer of security to our project.</p>

<h2 id="the-data">The Data</h2>

<p>For the purposes of this tutorial, I am changing the data from the original project. You may use the sample data, <a href="https://github.com/FreyGeospatial/freygeospatial.github.io/tree/master/_data" target="_blank">here</a></p>

<h2 id="virtual-machine-creation">Virtual Machine Creation</h2>
<ol>
  <li>
    <p><strong>Create an AWS Account if you do not already have one</strong></p>

    <ul>
      <li>
        <p>If you are a student, you can open an AWS Educate account, which gives you $40 of free credits per month as of December 2019</p>
      </li>
      <li>
        <p>You can also open a free-tier account. You may still need to enter in credit card information. However, the charges for this tutorial should be minimal (in the single dollars)</p>
      </li>
      <li>
        <p><em>Please, please, please</em> do not use the root user to create or manage resources for your account. This is a major security flaw and just bad practice. Instead, use a separate IAM user to do this- and as always, enable MFA. I will demonstrate how in another tutorial. Until then, please search for information on how to do this if you are unsure of the methodology.</p>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>Search for EC2 in the management console search bar, and go to the EC2 page.</strong></p>
  </li>
  <li><strong>Generate a Key Pair</strong>
    <ul>
      <li>This option will be under the “Network and Security Subsection”</li>
    </ul>

    <p><br /></p>

    <p><img src="/images/postgis-ec2/Key-pair-page.png" alt="Key-Pair Subsection" /></p>

    <p><br /></p>

    <p><img src="/images/postgis-ec2/Key-pair-generation.png" alt="Key-Pair Generation" /></p>

    <ol>
      <li>Choose a name for your key pair that is easy to remember</li>
      <li>If you are using Linux, MacOS, or a modern Windows OS (v10 or higher), use .pem file format</li>
      <li>
        <p>Download the key pair and store it in a safe place. <strong><em>Do not share it!</em></strong></p>

        <p><img src="/images/postgis-ec2/keep-it-safe.gif" alt="Keep-it-safe" /></p>
      </li>
    </ol>
  </li>
  <li><strong>Click Launch Instance button, and then choose an AMI (Amazon Machine Image)</strong>
    <ol>
      <li>
        <p>We chose an Ubuntu instance, which is Free Tier Eligible. But, Amazon Linux is also free-tier eligible as well.
 <img src="/images/postgis-ec2/AMI-Image.png" alt="AMI-Image" /></p>
      </li>
      <li>The t2.micro instance is the only one available for Free Tier, which will be fine for getting start
        <ul>
          <li>Each instance type is optimized for different purposes. Choose one at your discretion
 <img src="/images/postgis-ec2/Instance-type.png" alt="AMI-Image" /></li>
        </ul>
      </li>
      <li>Select the key pair you just created.</li>
      <li>Select your network settings
        <ul>
          <li>For our purposes, we left this as the default option. You may wish to change these going forward, however.</li>
          <li>Using the default VPC can allow you to have your resource available to the public internet. If you do not want or require this, choose a custom option.
            <ul>
              <li>Do not worry about your EC2 instance being public even if choosing the default option; you still need the key-pair .pem file you generated earlier. So, again, do not lose it and keep it safe.</li>
            </ul>
          </li>
        </ul>

        <p><img src="/images/postgis-ec2/networking.png" alt="Networking" /></p>
      </li>
      <li>Configure your storage
        <ul>
          <li>We left this as the default</li>
        </ul>

        <p><img src="/images/postgis-ec2/storage.png" alt="Storage" /></p>
      </li>
    </ol>
  </li>
  <li><strong>Click Review and Launch</strong></li>
  <li><strong>Instance should now be running. Check for it in the dashboard.</strong>
    <ul>
      <li>Remember to stop the instance when you are not using it; AWS charges per minute of EC2 usage.</li>
      <li>A stopped instance will still incur charges for use of storage. To completely stop charges, the instance must be deleted.</li>
    </ul>
  </li>
</ol>

<h2 id="vm-connection">VM Connection</h2>
<ol>
  <li>Under your available EC2 instances, click on the one you just created and then click the “connect” button</li>
  <li>Click the “SSH Client” tab
    <ul>
      <li>This will give you instructions on how to use an SSH client to log into your instance remotely.</li>
      <li>MacOS, Linux, and modern Windows OS should have an SSH client pre-installed. On MacOS, this is the terminal.</li>
    </ul>
  </li>
  <li>Open your SSH Client and <code class="language-plaintext highlighter-rouge">cd</code> into the directory containing your .pem file.</li>
  <li>Change the permissions on the .pem file.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">chmod 400 pem_filename.pem</code></li>
    </ul>
  </li>
  <li>Connect to the instance using its public dns:
 <code class="language-plaintext highlighter-rouge">ssh -i "filename.pem" &lt;public_DNS_address&gt;</code></li>
</ol>

<h2 id="setting-up-new-users-on-an-ubuntu-instance">Setting up new users on an Ubuntu Instance</h2>
<ol>
  <li>Log into the instance from your SSH Client</li>
  <li>Check the currently logged-in user by entering <code class="language-plaintext highlighter-rouge">who</code> into the command line
    <ul>
      <li>you can get a list of all users on the instance by typing <code class="language-plaintext highlighter-rouge">cut -d: -f1 /etc/passwd</code></li>
    </ul>
  </li>
  <li>Run the command <code class="language-plaintext highlighter-rouge">sudo adduser &lt;username&gt;</code> and follow the prompts
    <ul>
      <li>remember to save the credentials somewhere safe</li>
    </ul>
  </li>
  <li>Give the user sudo permissions: <code class="language-plaintext highlighter-rouge">sudo usermod -aG sudo &lt;username&gt;</code></li>
  <li>Switch the active user to the one you just created: <code class="language-plaintext highlighter-rouge">su - &lt;username&gt;</code>
    <ul>
      <li>Note: it is bad practice to use the root user</li>
    </ul>
  </li>
</ol>

<h2 id="installing-postgresql-on-ec2-ubuntu-instance">Installing PostgreSQL on EC2 Ubuntu Instance</h2>
<ol>
  <li><code class="language-plaintext highlighter-rouge">sudo apt update</code>
    <ul>
      <li>This command is used to update package information</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">sudo apt upgrade</code>
    <ul>
      <li>This command upgrades the packages based on the information downloaded in the previous update command</li>
      <li>If you are prompted to reboot the instance, do so. Then, log back into the instance with the user you have created</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">sudo apt install postgresql postgresql-contrib</code>
    <ul>
      <li>This command installs postgres on the instance</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">sudo apt-get install postgis</code>
    <ul>
      <li>This command installs the required files to run the PostGIS extension for Postgres</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">sudo systemctl start postgresql.service</code>
    <ul>
      <li>This command starts running Postgres. If you stop your EC2 instance, you will need to run this line again.</li>
    </ul>
  </li>
</ol>

<h2 id="setting-up-the-postgres-database">Setting up the postgres database</h2>
<ol>
  <li>Open an interactive shell session as the user postgres with elevated privileges. Let’s use: <code class="language-plaintext highlighter-rouge">sudo -i -u postgres</code>
    <ul>
      <li>this is slightly different from using the <code class="language-plaintext highlighter-rouge">su</code> command as thisopens a new shell session and environment</li>
    </ul>
  </li>
  <li>Create a new user: <code class="language-plaintext highlighter-rouge">createuser --interactive</code> (I named mine dbadmin)
    <ul>
      <li>remember to make this a superuser</li>
      <li>Note that you can only create new superuser roles if you are creating them while logged in from a role that is already a superuser. By default, the postgres role is a superuser, which is what you should be logged in as.</li>
    </ul>
  </li>
  <li>start using the postgres command line interface by typing <code class="language-plaintext highlighter-rouge">psql</code> into your ssh shell
    <ul>
      <li>You can always exit psql by entering <code class="language-plaintext highlighter-rouge">\q</code> into the command line</li>
    </ul>
  </li>
  <li>Check that the new user has been created by entering <code class="language-plaintext highlighter-rouge">\du</code> into the <code class="language-plaintext highlighter-rouge">psql</code> console</li>
  <li>Switch to the new role you just created:
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">-- remember, you can always use SQL syntax in the psql command line</span>
 <span class="k">SET</span> <span class="k">ROLE</span> <span class="o">&lt;</span><span class="n">role_name</span><span class="o">&gt;</span><span class="p">;</span>

 <span class="c1">-- Check that you are switched to the new role:</span>
 <span class="k">SELECT</span> <span class="k">current_role</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
  <li>Create a new database with the desired name by typing in standard sql code:
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">CREATE</span> <span class="k">DATABASE</span> <span class="n">my_postgis_db</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
  <li>Make sure that you have created the database asnd it is owned by the new role you have created by typing <code class="language-plaintext highlighter-rouge">\l</code> into the <code class="language-plaintext highlighter-rouge">psql</code> console
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="c1">-- This SQL code will also give you the same information as `\l`</span>
 <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">pg_database</span><span class="p">;</span> <span class="c1">-- to exit the console menu if stuck, click the `q` key on your keyboard.</span>
</code></pre></div>    </div>
  </li>
  <li>Enable PostGIS capabilities
    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">CREATE</span> <span class="n">EXTENSION</span> <span class="n">PostGIS</span><span class="p">;</span>

 <span class="c1">-- check to make sure it is now enabled:</span>
 <span class="k">SELECT</span> <span class="n">extname</span> <span class="k">FROM</span> <span class="n">pg_extension</span> <span class="k">where</span> <span class="n">extname</span> <span class="o">=</span> <span class="s1">'postgis'</span><span class="p">;</span>
</code></pre></div>    </div>
  </li>
</ol>

<h3 id="notes">Notes:</h3>
<p>For any role used to log into Postgres, that role will attempt to connect to a database of the same name by default. You can create the appropriate database with the <code class="language-plaintext highlighter-rouge">createdb</code> command and adjust what database to connect to.</p>

<h2 id="enabling-gui-on-ec2-not-recommended-for-this-tutorial">Enabling GUI on EC2 (not recommended for this tutorial)</h2>
<p><strong>Important consideration for GUI use:</strong> As of writing this, CPU credits are limited to about 144 credits for the free-tier eligible micro EC2 instance. These credits are regenerated every 24 hours (6 credits per hour). <em>GUI use seems to monopolize CPU usage and was not necessary for our work.</em> It will also incur performance costs. More info about CPU and credit usage <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-credits-baseline-concepts.html" target="_blank">here</a>.</p>

<p>However, to remote in with a GUI, we took these steps:</p>
<ol>
  <li>Edited the security group settings in the AWS Management Console for the instance for RDP import access port</li>
  <li>To remote in from a Windows local machine, open Remote Desktop Connection and enter “ec2-x-xx-xxx-xx.us-east-1.compute.amazonaws.com”. For Linux, use the VNC viewer.</li>
</ol>

<h2 id="postgres-data-import">Postgres Data Import</h2>
<p>Copy local files onto the AWS EC2 Ubuntu Virtual Machine. Make sure you have the .pem file handy.</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">scp -i path\to\pem\file -r C:\Users\username\directory\of\files user@ec2-xx-xxx-xx-xx.us-east-2.compute.amazonaws.com:~/path/to/save/location</code>
​- To upload a single file instead of a directory, remove the <code class="language-plaintext highlighter-rouge">-r</code> (recursive) option.</li>
</ul>

<p>You could also use the PostGIS Shapefile Import/Export Manager as an alternative. But that is a GUI application, is less reproducible and cannot be automated.</p>

<h2 id="next-steps">Next Steps:</h2>

<h3 id="using-ogr2ogr">Using ogr2ogr</h3>

<p>Use the <code class="language-plaintext highlighter-rouge">ogr2ogr</code> command to upload vector spatial data into the database. This can be used for a multitude of file types, but the general structure of the code stays the same (see that the code chunks below are almost identical). Note that the option <code class="language-plaintext highlighter-rouge">-lco precision=NO</code> and argument are often necessary when importing shapefiles, which contain field size limits. If uploading data to to postgres that has been copied to the virtual machine, you must now connect to EC2 instance prior to following the next steps. Instructions on connecting to EC2 are found earlier in this markdown.</p>

<p>For uploading Shapefiles and GPX files, respectively:</p>

<p><code class="language-plaintext highlighter-rouge">ogr2ogr -f "PostgreSQL" PG:"host=localhost user=postgres dbname=postgres password=PASSWORD port=5432" FILE_PATH.shp -overwrite -lco precision=NO -lco GEOMETRY_NAME=geom -nln "NAME_OF_NEW_TABLE"</code></p>

<p><code class="language-plaintext highlighter-rouge">ogr2ogr -f "PostgreSQL" PG:"host=localhost user=postgres dbname=postgres password=PASSWORD port=5432" FILE_PATH.gpx -overwrite -lco GEOMETRY_NAME=geom -nln "NAME_OF_NEW_TABLE"</code></p>

<p>If you encounter an error, you might need to download GDAL from https://gdal.org/download.html and paste the folder into your postgres program folder.</p>

<h3 id="importing-csvs-with-psql">Importing CSVs with psql</h3>

<p>First, create the table you wish to upload data to:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="k">schema</span><span class="p">.</span><span class="k">table_name</span> <span class="p">(</span>
<span class="n">column_name1</span> <span class="nb">text</span><span class="p">,</span>
<span class="n">lat</span> <span class="nb">double</span> <span class="nb">precision</span><span class="p">,</span>
<span class="n">lon</span> <span class="nb">double</span> <span class="nb">precision</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Then, use the <code class="language-plaintext highlighter-rouge">COPY</code> command to load the CSV to the table:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">copy</span> <span class="n">ch01</span><span class="p">.</span><span class="n">restaurants_staging</span>
<span class="k">FROM</span> <span class="s1">'CSV_file_path.csv'</span> 
<span class="k">DELIMITER</span> <span class="k">as</span> <span class="s1">','</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="securing-the-virtual-machine-and-postgresql-database">Securing the Virtual Machine and PostgreSQL Database</h3>

<p><a href="This article" target="_blank">https://www.digitalocean.com/community/tutorials/how-to-secure-postgresql-against-automated-attacks</a> contains useful information on securing the database. It helped us set up a firewall on our EC2 instance while only allowing our specific IP addresses through them.</p>

<p>To do this we used the following commands:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sudo ufw status</code> - To check the status the firewall</li>
  <li><code class="language-plaintext highlighter-rouge">sudo ufw enable</code> - To enable the firewall</li>
  <li><code class="language-plaintext highlighter-rouge">sudo ufw allow from &lt;client-ip&gt; to any port 5432</code> - restricts database access to specific IP addresses</li>
</ul>

<h3 id="accessing-the-ec2-postgres-database-on-pgadmin">Accessing the EC2 Postgres Database on PgAdmin</h3>
<ul>
  <li><a href="https://timothybramlett.com/exposing-postgresql-to-remote-connections-from-only-a-single-ip-in-aws.html" target="_blank">This guide</a> was very helpful in the second step of getting IP addresses specifically set up for use of the database with Postgres.</li>
  <li>To keep security strong, we elected to add only the IP addresses we will be using as the allowable ip addresses to access the database.</li>
</ul>

<p>We had to augment the <code class="language-plaintext highlighter-rouge">pg_hba.conf</code> file to change the allowable ip addresses.</p>

<p><b>Note:</b> We had an error where the database was not starting up after changing the pg_hba.conf file. The error arose because we did not include a /xx notation at the end of an ip address and the invalid IP was preventing the server from starting up.</p>

<h3 id="data-visualization">Data Visualization</h3>
<p>To visualize our data stored in the Postgres database, we chose to use a combination of Geoserver and Leaflet</p>

<h3 id="geoserver-set-up">Geoserver Set-up</h3>
<ol>
  <li>Geoserver was set-up on our local machines, following <a href="https://www.e-education.psu.edu/geog585/node/686" target="_blank">this guide</a> .</li>
  <li>Connected to our Postgres database by creating a new “Store”, then adding our EC2 and database parameters.</li>
  <li>Styling layers</li>
</ol>

<p><img src="/images/postgis-ec2/geoserver.png" alt="Geoserver" /></p>

<p><br /></p>

<h3 id="leaflet-visualization">Leaflet Visualization</h3>
<p>After setting up GeoServer, we pulled data into Leaflet.js to visualize it as a web map. The code for our web map can be found <a href="https://hackmd.io/_n0H0mD5TSaGV6kLmgGvNQ" target="_blank">here on hackmd.io</a>, along with additional details regarding EC2 security/firewall implementation.</p>

<p><img src="/images/postgis-ec2/web_map.png" alt="web map" /></p>]]></content><author><name>Jordan Frey</name></author><category term="Data Engineering" /><category term="Postgres/PostGIS" /><category term="SQL" /><category term="AWS" /><category term="GIS" /><category term="Geoserver" /><category term="EC2" /><summary type="html"><![CDATA[This semester, my peers Aaron Eubank, Bryce Stouffer, Samuel Watson, Wei Hong Loh, and I took on a directed study under our professor Dr. Lyndon Estes on the implementation and use of PostGIS. While I had prior experience with SQL Server and some of its spatial functions, I had never used SQL to perform a full range of geoprocessing tasks. I found my workflows to be more reproducible than using ArcGIS and QGIS, and found processing speeds to be faster than R’s {sf} package.]]></summary></entry></feed>