<?xml version="1.0" encoding="UTF-8" ?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" version="2.0"><channel><title>Steve Pousty | CrunchyData Blog</title>
<atom:link href="https://www.crunchydata.com/blog/author/steve-pousty/rss.xml" rel="self" type="application/rss+xml" />
<link>https://www.crunchydata.com/blog/author/steve-pousty</link>
<image><url>https://www.crunchydata.com/build/_assets/default.png-W4XGD4DB.webp</url>
<title>Steve Pousty | CrunchyData Blog</title>
<link>https://www.crunchydata.com/blog/author/steve-pousty</link>
<width>256</width>
<height>256</height></image>
<description>PostgreSQL experts from Crunchy Data share advice, performance tips, and guides on successfully running PostgreSQL and Kubernetes solutions</description>
<language>en-us</language>
<pubDate>Fri, 19 Feb 2021 12:02:22 EST</pubDate>
<dc:date>2021-02-19T17:02:22.000Z</dc:date>
<dc:language>en-us</dc:language>
<sy:updatePeriod>hourly</sy:updatePeriod>
<sy:updateFrequency>1</sy:updateFrequency>
<item><title><![CDATA[ Using PostgreSQL to Shape and Prepare Scientific Data ]]></title>
<link>https://www.crunchydata.com/blog/1-using-postgresql-to-shape-and-prepare-scientific-data</link>
<description><![CDATA[ Today we are going to walk through some of the preliminary data shaping steps in data science using SQL in Postgres. ]]></description>
<content:encoded><![CDATA[ <p>Today we are going to walk through some of the preliminary data shaping steps in data science using SQL in Postgres. I have a long history of working in <a href=https://en.wikipedia.org/wiki/Data_science>data science</a>, including my Masters Degree (in Forestry) and Ph.D. (in Ecology) and during this work I would often get raw data files that I had to get <a href=https://towardsdatascience.com/the-ultimate-guide-to-data-cleaning-3969843991d4>into shape</a> to run analysis.<p>Whenever you start to do <a href=https://lifehacker.com/the-science-behind-how-we-learn-new-skills-908488422>something new</a> there is always <a href=https://start.jcolemorrison.com/my-process-for-learning-new-technology-skills/>some uncomfortableness</a>. That “why is this so hard” feeling often stops me from trying something new, but not this time! I attempted to do most of my data prep and statistical analysis within <a href=https://www.crunchydata.com/products/crunchy-certified-postgresql>PostgreSQL</a>. I didn’t allow myself any copy and paste in google sheets nor could I use <a href=/blog/mapping-data-types-in-plpython>Python</a> for scripting. I know this is hard core and in the end may not be the “best” way to do this. But if I didn’t force myself to do it, I would never understand what it could be like.<h2 id=the-project><a href=#the-project>The Project</a></h2><p>Next I needed to pick a modeling project. We have a few interesting data sets in the Crunchy Data Demo <a href=https://github.com/CrunchyData/crunchy-demo-data>repository</a> but nothing that looked good for statistical modeling. I thought about trying to find something a little more relevant to my life.<p><img alt=50324073763_17e791abe1_c loading=lazy src=https://f.hubspotusercontent00.net/hubfs/2283855/50324073763_17e791abe1_c.jpg><blockquote><p>Photo by keppet on flickr</blockquote><p>You may have noticed that we are having a few fires here in Northern California. Quite a few Crunchy folks live here. I'm in Santa Cruz (with a fire that was less than 7 miles away), Craig lives near Berkeley, Daniel and Will live in SF. We've compared notes on the sky (fun pic here), the amount of ash on cars, and <a href="https://www.purpleair.com/map?opt=1/i/mAQI/a10/cC0#10.69/37.0502/-122.0166">PurpleAir</a> values.<p>I thought it would be interesting to see if logistic regression could predict the probability of a fire based on the historical weather and fire patterns. All the details for how we obtained historical California fire data <a href=https://github.com/CrunchyData/crunchy-demo-data/tree/master/calfire>is here</a> and here is the <a href=https://github.com/CrunchyData/crunchy-demo-data/tree/master/calweather>weather data</a>.<p>Here is a preview of the fire data:<p><img alt="Screenshot of the fire data"loading=lazy src=https://f.hubspotusercontent00.net/hubfs/2283855/Google%20Drive%20Integration/1%20Using%20PostgreSQL%20to%20shape%20and%20prepare%20scientific%20data-1.png><p>And here is a preview of the weather data:<p><img alt="Screenshot of the weather data"loading=lazy src=https://f.hubspotusercontent00.net/hubfs/2283855/Google%20Drive%20Integration/1%20Using%20PostgreSQL%20to%20shape%20and%20prepare%20scientific%20data.png><h2 id=initial-import-of-data><a href=#initial-import-of-data>Initial Import of Data</a></h2><p>You can see on the pages above some of the initial cleaning we did. Most of it is minor though crucial things like renaming columns, taking a separate date and time column, and combining them into a timestamp, or changing the value on an obvious typo.<p>Let’s dig into the concatenating the date and time into a timestamp for the weather data (step 5). The old me would have put all the data in a spreadsheet and then used a concatenate function to put the two together. I would do one cell with the formula and then copy and paste into all the cells.<p>Instead, this is all we need:<pre><code class=language-pgsql>UPDATE weather SET date_time = to_timestamp((date || ' ' || time || '-7'),
'YYYYMMDD HH24:MI');
</code></pre><p>One of the obvious advantages you should see right away is that we only have one line of code to accomplish what would take several manual steps. Since it’s in code if we need to reimport the data or modify it for another data set, we just have to reuse this line. We could even use this as part of an automated script.<p>The other thing you should notice is that we were able to actually add a timezone to the data (the -7). And finally, we were able to act on the data as a whole unit. That means we do the operation all at once without fat-fingering the data or forgetting to paste some sort of rows. The data stays together and all the operations happen at “the same time”. If we wanted to be even more careful we could have wrapped the statement in <a href=https://www.postgresql.org/docs/current/tutorial-transactions.html>a transaction</a>.<h2 id=subsetting-our-data><a href=#subsetting-our-data>Subsetting Our Data</a></h2><p>The first thing we are going to do to shape our data is subset the fires. Our weather station is in Northern California and so it probably doesn’t make sense to try and predict Southern California fire probability given weather in the north. This is again a one liner to subset our data:<pre><code class=language-pgsql>SELECT fire19.* INTO ncalfire FROM fire19, fire19_region WHERE
*st_covers\*(fire19_region.geom, fire19.geom) AND fire19_region.region =
'Northern';
</code></pre><p>We selected all the columns from the original data where the Northern Region covers all the fire data. We use <code>ST_Covers</code> rather than <code>ST_Contains</code> because of some <a href=https://lin-ear-th-inking.blogspot.com/2007/06/subtleties-of-ogc-covers-spatial.html>unexpected behaviour</a> in <code>ST_Contains</code>. By using <code>st_covers</code>, our operation will not include any fires whose boundaries go outside the Northern Region, for example if the fire spread into Oregon.<h2 id=getting-ready-for-logistic-regression><a href=#getting-ready-for-logistic-regression>Getting ready for Logistic Regression</a></h2><p>Logistic regression needs the response variable (did a fire happen on that day), be coded as a 1 = fire happened, 0 = no fire. When I started to examine the data, as I should have known based on living in California, multiple fires can start on the same day. So we need to aggregate data from multiple days into a single entry. With SQL we don’t have to lose any information. Here is the one line that does what we need:<pre><code class=language-pgsql>WITH grouped_fire AS (
 SELECT alarm_date, count(*) as numfires, string_agg(fire_name, ', ') AS names,

*st_collect*(geom)::geometry(geometrycollection, 3310) AS geom FROM ncalfire
GROUP BY alarm_date
 )
SELECT w.*, grouped_fire.\*, 1 AS hasfire INTO fire_weather FROM weather w, grouped_fire
WHERE grouped_fire.alarm_date = w.date_time::date;
</code></pre><p>So the first part of this query starting <code>WITH grouped_fires</code> until the <code>)</code> is called <a href=/blog/with-queries-present-future-common-table-expressions>a <abbr>CTE</abbr></a> or <dfn>common table expression</dfn>. We do all our aggregation in the CTE based on <code>alarm_date</code> (specified by the <code>GROUP BY alarm_date</code>). Our output will be for each alarm date:<ol><li>Return the number of fires that happened on that day.<li>Aggregated all the strings for the fire names into one string separated by “,” (we could have also used an an array of string).<li>Collect the geometries of all the different fires on that day into one geometry collection.</ol><p>Now we can use the information from the CTE like a table in the second part of the query. In the next part we grab all the columns from the CTE along with all the columns in the original table and then we just put a 1 in for all entries in an output column named <code>hasfire</code>.<p>As a side note, I am a big fan of bringing clarity to boolean columns by prefixing them with <code>is</code> or <code>has</code> so you know what the true condition means.<p>Then we join the CTE date to the original table by matching dates. We truncate the timestamp column by casting it to a date. Notice we are selecting into a new table and the output of this command will actually go into a table rather than showing on the screen. With that we now have a table with all the weather data, dates of fires, and a 1 in the <code>hasfire</code> column.<h2 id=fixing-our-geometry><a href=#fixing-our-geometry>Fixing our Geometry</a></h2><p>You will notice above we had to make a geometrycollection to group the fires geometries together. Most desktop and other GIS software don’t know what to do with a geometrycollection geometry type. To fix this problem we are going to cast the geometries to multipolygons. A multipolygon allows a single row to have multiple polygons. The easiest example of this is a town or property that includes islands. A multipolygon allows the town, along with its islands, to be seen as a single entry in the table. Here is the call we use:<pre><code class=language-pgsql>ALTER TABLE fire_weather ALTER COLUMN geom type geometry(multipolygon,3310)
USING st_collectionextract(geom, 3);
</code></pre><p><a href=https://postgis.net/docs/ST_CollectionExtract.html><code>ST_collectionextract</code></a> takes a multigeometry and collects all the geometries you ask for and returns a multigeometry of the requested type. The 3 in <code>st_collectionextract</code> is the “magic number” meaning polygons<h2 id=creating-our-non-fire-data><a href=#creating-our-non-fire-data>Creating our non-fire data</a></h2><p>We created our fire data and fixed it’s geometry. Now we have to go back to the weather data and assign 0 to all the days where there was no fire:<pre><code class=language-pgsql>WITH non*fire_weather AS (
SELECT weather.* FROM weather WHERE id NOT IN (SELECT id FROM fire*weather)
)
SELECT non_fire_weather.*, null::date AS alarm_date, 0::bigint AS numfires,
null::text AS names, null::geometry(MultiPolygon,3310) AS geom, 0 AS hasfire
INTO non_fire_weather FROM non_fire_weather;
</code></pre><p>Again we are going to start with a CTE but this time the first query is used to find all the days that are NOT in the new fire + weather table above. This gives us a “table” with only weather data on days where a fire did not occur. Then in the second part of the query we add in all the columns that we added to our <code>fire_weather</code> table. Finally we put the results into a new table called <code>non_fire_weather</code>.<p>Now we combine the <code>fire_weather</code> and <code>non_fire</code> weather tables into one master table called <code>alldata</code>:<pre><code class=language-pgsql>SELECT _ INTO alldata FROM non_fire_weather UNION SELECT _ FROM fire_weather;
</code></pre><p>We did this merging with the full data set in case we want to do other types of analyses with this data. For example, we actually have counts of fire on days and we also have total area of fire burned (from the polygons). These variables might lead to other interesting analysis in the future.<h2 id=wrap-up><a href=#wrap-up>Wrap Up</a></h2><p>Let’s wrap up and look at all the good work we were able to do using SQL for data science. We took some tables imported from raw data, turned dates and time into more usable formats, aggregated the fire events, kept the geometries intact, created a code for event versus not event, and created a large master data set.<p>All of this was done with lines of code, no manual steps. Doing our data shaping this way makes our process easily repeatable if we need to do it again. We could actually put it in a script and automate the importing of new data. On a personal note, it makes my life so easy if I need to create a new version of the dataset or import the data to a new server.<p>I hope you found this code and the examples useful in your data science work (here is the <a href=https://github.com/thesteve0/postgres-fire>github repo</a> for it). What is your experience with using SQL to shape your data before doing analysis? Do you have some tips or tricks you would like to share? Leave us comment on the <a href=https://twitter.com/crunchydata>Crunchy Data twitter account</a>. Have fun with your analysis and code on!<p>Cover image By <a href=https://thenounproject.com/annette2>Annette Spithoven</a>, NL ]]></content:encoded>
<category><![CDATA[ Fun with SQL ]]></category>
<author><![CDATA[ Steve.Pousty@crunchydata.com (Steve Pousty) ]]></author>
<dc:creator><![CDATA[ Steve Pousty ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/1-using-postgresql-to-shape-and-prepare-scientific-data</guid>
<pubDate>Fri, 19 Feb 2021 12:02:22 EST</pubDate>
<dc:date>2021-02-19T17:02:22.000Z</dc:date>
<atom:updated>2021-02-19T17:02:22.000Z</atom:updated></item>
<item><title><![CDATA[ R Predictive Analytics in Data Science Work using PostgreSQL ]]></title>
<link>https://www.crunchydata.com/blog/r-predictive-analytics-in-data-science-work-using-postgresql</link>
<description><![CDATA[ Today we are going to finish up by showing how to use that stored model to make predictions on new data. By the way, I did all of the Postgres work for the entire blog series in Crunchy Bridge. I wanted to focus on the data and code and not on how to run PostgreSQL. ]]></description>
<content:encoded><![CDATA[ <p>Greetings friends! We have come to our final blog post in my series about the data science workflow using PostgreSQL. In the last <a href=/blog/using-r-in-postgres-for-logistic-regression-modeling-1>blog post</a>, we used <a href=https://access.crunchydata.com/documentation/plr/8.3.0.18/>PL/R</a> to create a function which returns the output from a logistic regression model trained on our fire data. We then took that model object and stored it into a separate table.<p>Today we are going to finish up by showing how to use that stored model to make predictions on new data. By the way, I did all of the Postgres work for the entire blog series in <a href=https://www.crunchydata.com/products/crunchy-bridge>Crunchy Bridge.</a> I wanted to focus on the data and code and not on how to run PostgreSQL.<h2 id=numer-models><a href=#numer-models>Numer models</a></h2><p>When people make numer models they usually have one of two purposes in mind.<ol><li>Using the model to get a better understanding of the processes that leads to the outcome.<li>Trying to get the most accurate prediction of the outcome.</ol><p>These two options can sometimes be <a href=https://jtonedm.com/2011/04/11/predictive-models-are-not-statistical-models/>at odds</a> in your analysis.<p>You can usually make a more accurate prediction model using black box techniques such as neural networks or generalized additive models. But these techniques do not allow any inference about magnitude or directions of cause and effect. There are other reasons they can be at odds, such as predictive power versus parsimony of explanatory variables, but we are not going to go in depth on that today.<p>In our example, we are not particularly interested in an explanatory model since we have little ability to control the weather. Instead, we are more interested in a predictive model to give us fire occurrence prediction as accurately as possible.<p>The full use case for using this model would be:<ol><li>Run a weather forecast model predicting tomorrow’s weather for the explanatory variables used in our logistic regression.<li>Plug those weather values into our logistic regression model (covered in today's blog post).<li>Return the predicted probability of tomorrow’s weather.<li>Give this information to someone who can use it to make better decisions.</ol><p>Our use case is very similar to other non-fire use cases. Here are some examples:<ol><li>Create a credit card <a href=https://towardsdatascience.com/credit-card-fraud-detection-9bc8db79b956>fraud model</a>, store it in the database, write a trigger on insert that predicts the probability of fraud.<li>Create <a href=https://hbr.org/1988/07/four-steps-to-forecast-total-market-demand>a model</a> that predicts market demand for an item, store it in the database, produce a report every morning that shows store managers how much inventory to order.<li>Build a predictive model of your favorite football team (yes I mean the <a href=https://content.iospress.com/articles/journal-of-sports-analytics/jsa190314>U.S. version</a> and the <a href=https://towardsdatascience.com/the-shape-of-football-games-1589dc4e652a>International version</a>), update based on stats from their last game/match, predict their likelihood of winning their next match.</ol><p>Let’s get into the final step on our data science journey.<h2 id=using-the-r-model-to-predict-fire-probability><a href=#using-the-r-model-to-predict-fire-probability>Using the R model to predict fire probability</a></h2><p>We are going to use PL/R to do the statistical work, just like we did in the <a href=/blog/using-r-in-postgres-for-logistic-regression-modeling-1>last post</a>. Unless you know R and PL/R, I highly recommend you go back and read it. I am not going to cover the benefits of PL/R or the pieces of setting up a PL/R function.<p>At the end of last post we took the R object for a logistic regression model and stored it in a bytea column along with some metadata columns (including a timestamp column for record insertion).<h2 id=the-new-function><a href=#the-new-function>The new function</a></h2><p>The prize we get for using R and storing the model object in a table is the ability to write a simple function to generate a prediction. It’s so small I am going to paste the entire function here:<pre><code class=language-pgsql>CREATE OR REPLACE FUNCTION final.predict_logistic( model bytea, newdata
final.test, out probability numeric)

as $$

probability &#60- predict(model, data.frame(newdata), type = 'response')

return(probability)

$$
LANGUAGE 'plr';
</code></pre><p>We are creating this function in the <code>final</code> schema. The bytea we are passing in will be the R model we stored in the table.<p>The declaration for the <code>newdata</code> parameter might be new for some people. If <a href=/blog/randomly-sampling-data-using-sql-and-postgresql>you</a> <a href=/blog/randomly-sampling-data-using-sql-and-postgresql>recall</a>, the <code>test</code> table contains data we separated out from the training data set and has the same schema as the <code>data</code> table. By declaring it of type <code>final.test</code> we are indicating that we expect a record to be passed that matches the schema of the table in the <code>final</code> schema named <code>test</code>.<p>Finally we declare that we are returning, via an out variable, the calculated probability.<h2 id=one-line-of-r-code><a href=#one-line-of-r-code>One line of R code</a></h2><p>The actual code of our function is <strong>1 LINE</strong>! I could have returned the call to <code>predict</code>, but I like separating those calls out for clarity.<p>This line also shows why R is such a powerful language for statistics and data science. Built-in are core functions for most of the methods data scientists and statisticians use. It is a Domain Specific Language for statistics and data science.<p>Our call to <a href=https://www.rdocumentation.org/packages/stats/versions/3.6.2/topics/predict.glm><code>predict</code></a>:<ol><li>Takes a R model created with a linear or generalized linear model (our stored model).<li>Takes a data frame which contains one or more rows of data that matches the independent variables used in the model.<li>We then tell the function we want the predicted response rather than the standard errors on the response.<li>The function returns a value between 0 and 1, representing the probability of fire.</ol><p>And that’s it!<p>Our function does all we need it to do. The data scientist gives this function to the DBA and they put it in the database. Now, application developers can just pass in data and get back a prediction of fire probability and the data scientist can sleep soundly because nobody can “mess up” implementing a prediction function.<h2 id=calling-our-function-in-sql><a href=#calling-our-function-in-sql>Calling our function in SQL</a></h2><p>Using this function is quite simple since we only need to pass in 2 parameters.<pre><code class=language-pgsql>SELECT final.predict_logistic(

newdata := (SELECT f.\*::final.test FROM final.test AS f ORDER BY f.date_time
limit 1),

model := (SELECT model AS the model FROM final.logistic_models AS l ORDER BY
l.created_at DESC LIMIT 1) ) AS fire_probability;
</code></pre><p>Passing in the new explanatory data we do a <code>select</code> on <code>final.test.*</code> and cast it to <code>final.test</code> like the function expects. To limit the rows passed in, submit the oldest data value.<p>Pass in the R model, we select the model column from the <code>final.logistic_models</code> table. Again, we sort the results but this time we only return the newest model. Voila!<p>To make this easier for application developers to use, you might want to make this query into a view.<h2 id=wrapping-up-the-whole-series><a href=#wrapping-up-the-whole-series>Wrapping up the whole series</a></h2><p>With this post we have come to the conclusion of <a href=/blog/author/steve-pousty>our series</a> on using Postgres and SQL in a data science/research workflow.<p>We covered all these topics without using any code running outside of Postgres:<ol><li>Cleaning up the data<li>Massaging the data to fit our needs<li>Subsetting the data<li>Combining two data sets to create the data we needed for analysis<li>Randomly sampled our table into training and testing data<li>Created a function to create new columns that contained the centered and standardized values for the explanatory variables<li>Created another function to fit a logistic regression model with R using the centered and standardized data<li>Stored the R logistic regression model in a new table<li>Finally, wrote a function that accepts the stored model and new data to predict the probability of a new fire</ol><p>If I had told you, before this series, that we were going to do all this work inside PostgreSQL, I bet the response for most of you would have been “no way!”. And that was the point of this series.<p>Like my coworker <a href=https://twitter.com/craigkerstiens>Craig</a> said:<blockquote><p>“You probably won’t get them all the way there, but you will have opened their world to see what is possible.”</blockquote><p>One of the messages I hope I imparted to you was the clear separation of concerns you can achieve by using PostgreSQL as the central node in your architecture.<p>The data can be cleaned and duplicated with strong permission handling. The data scientists can work with the db data in their native environment to come up with the algorithms. The DBA or sysadmin can then put those algorithms into the database as functions available to application developers.<p>Finally the application developer gets the benefit of not having to rewrite the algorithm in their language, instead they can just do a simple SQL call. Every different “user” of the database gets to interact with the database for their needs without reaching into other “users” domains.<p>One other benefit of using the approach outlined here is that all these steps can be put in a text file and run against the database. You don't have to go back and copy and paste to recreate data in a spreadsheet. All your steps are clearly laid out in code that can be rerun anytime you need.<p>My final goal was to show how learning more SQL would lead to big benefits in your day to day work. I was pleasantly surprised by the functionality I never knew existed in PostgreSQL, like randomly sampling a table.<p>Did this series make you think more about how you use PostgreSQL? Have you used PostgreSQL in data science workflows? I would love to hear more about your experience and insights by sending messages to our <a href=https://twitter.com/crunchydata>twitter account</a>.<p>Happy data sleuthing! ]]></content:encoded>
<category><![CDATA[ Fun with SQL ]]></category>
<author><![CDATA[ Steve.Pousty@crunchydata.com (Steve Pousty) ]]></author>
<dc:creator><![CDATA[ Steve Pousty ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/r-predictive-analytics-in-data-science-work-using-postgresql</guid>
<pubDate>Tue, 12 Jan 2021 04:00:00 EST</pubDate>
<dc:date>2021-01-12T09:00:00.000Z</dc:date>
<atom:updated>2021-01-12T09:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Using R in Postgres for Logistic Regression Modeling ]]></title>
<link>https://www.crunchydata.com/blog/using-r-in-postgres-for-logistic-regression-modeling-1</link>
<description><![CDATA[ Greetings friends! We have finally come to the point in the Postgres for Data Science series where we are not doing data preparation. Today we are going to do modeling and prediction of fire occurrence given weather parameters… IN OUR DATABASE! ]]></description>
<content:encoded><![CDATA[ <p>Greetings friends! We have finally come to the point in the Postgres for Data Science <a href=/blog/author/steve-pousty>series</a> where we are not doing data preparation. Today we are going to do modeling and prediction of fire occurrence given weather parameters… IN OUR DATABASE!<p>Quick recap:<ol><li>We found <a href=/blog/1-using-postgresql-to-shape-and-prepare-scientific-data>some data</a> on historical fires and historical weather data for California.<li>We fixed up the data and merged it to make it more appropriate for logistic regression.<li>We <a href=/blog/randomly-sampling-data-using-sql-and-postgresql>randomly sampled</a> non-fire data and split the data into training and verification data sets.<li>We put our final data for analysis into a PostgreSQL schema named “final”.<li>Finally, we standardized and centered our weather parameters in new columns in our <a href=/blog/using-plpgsql-to-calculate-new-postgres-columns>table</a>.</ol><p>For those of you not in statistics or data science, this may seem like a lot of work just to run a logistic regression. But as people in those fields will tell you, cleaning, shaping, and transforming your data for analysis is <a href=https://blog.ldodds.com/2020/01/31/do-data-scientists-spend-80-of-their-time-cleaning-data-turns-out-no/><em>actually</em></a> where you spend the majority of your time.<h2 id=statistical-modelling-with-plr><a href=#statistical-modelling-with-plr>Statistical modelling with PL/R</a></h2><p>For those who have been living under a rock and haven’t heard of <a href=https://www.r-project.org/>R</a>, it is a open-source programming language focused on statistics and data science with a <a href=https://cloud.r-project.org/web/packages/available_packages_by_name.html>HUGE ecosystem</a> behind it. While it may <a href=https://cloud.r-project.org/doc/manuals/r-release/R-intro.html>look strange</a> to you - it’s extremely well written for matrix calculations and statistical analysis.<p>As we have seen before, PostgreSQL has the ability to write functions in different programming languages. My colleague at Crunchy Data, <a href=https://joeconway.com/>Joe Conway</a>, did just that for R in an embedded language <a href=https://github.com/postgres-plr/plr>PL/R</a>. I don’t have time or space to teach PL/R in a blog post but there are a lot of good materials, including a hands-on tutorial and presentations. Today I will mostly show the flow a data scientist can use and some more interesting properties of PL/R.<h2 id=getting-data-from-postgresql-in-and-out-of-r><a href=#getting-data-from-postgresql-in-and-out-of-r>Getting data from PostgreSQL in and out of R</a></h2><p>One of the best properties Joe built into the language is <a href=https://www.rdocumentation.org/packages/RPostgreSQL/versions/0.6-2>RPostgresql</a> compatibility support. If you work in R, the driver you use to connect to PostgreSQL is RPostgresql. What Joe did was stub out all the RPostgresql connection methods in PL/R to become noops functions. Instead, PL/R assumes it is connecting to the current database. As the documentation says:<p><em>These functions nominally work like their RPostgreSQL counterparts except that all queries are performed in the current database. Therefore all driver and connection related parameters are ignored, and dbDriver, dbConnect, dbDisconnect, and dbUnloadDriver are no-ops.</em><p><strong>The implication of this for you, dear reader, is that you can write your normal RPostgresql R code on your desktop, and then almost drop it straight into PL/R function.</strong> Brilliant!<p>This pattern above is exactly what I did for my function development. I used my favorite (and best) IDE in the whole world, <a href=https://www.jetbrains.com/idea/>IDEA</a>, with its <a href=https://plugins.jetbrains.com/plugin/6632-r-language-for-intellij>R plugin</a> to do all my R work on my desktop. I just used RPostgresql to connect to the DB where I would be creating my function.<h2 id=creating-the-logistic-regression-model><a href=#creating-the-logistic-regression-model>Creating the logistic regression model</a></h2><p>Since my main purpose was to show the use of PostgreSQL in a data science/statistical analysis workflow, I did not go through the normal model fitting techniques. Instead, I looked at the independent variables that would seem related to fire risk:<ol><li>Minimum relative humidity<li>Maximum air temperature<li>Precipitation<li>Solar Radiation</ol><p>The parameters and overall model were all significant at p &#60 0.05 so I deemed it “good enough”.<p>Onward and upward!<h2 id=putting-the-model-into-plr><a href=#putting-the-model-into-plr>Putting the model into PL/R</a></h2><p>The next phase is to take the model off my desktop and put it on the Postgres server. Given the work Joe did before on RPostgresql compatibility, I made 0 changes to my R code when creating the PL/R function. Let’s go through the steps I did to create the function.<p>The first step was to add the PL/R extension to my database. I need to do this command as a superuser (typically the user “postgres”). I will also need to add the function as a superuser since PL/R is an <a href=https://subscription.packtpub.com/book/big_data_and_business_intelligence/9781783980581/8/ch08lvl1sec64/are-untrusted-languages-inferior-to-trusted-ones>untrusted language</a>. Note, <a href=https://learn.crunchydata.com/postgresql-devel/courses/beyond-basics/qplr/>this tutorial</a> actually teaches you how to make PL/R a trusted language, but please don’t do that on your production DBs unless you know what you are doing.<pre><code class=language-pgsql>CREATE EXTENSION plr;
</code></pre><p>And with that we are ready to add our function.<h2 id=turn-my-r-code-into-a-postgres-function><a href=#turn-my-r-code-into-a-postgres-function>Turn my R code into a Postgres function</a></h2><p>The code for the function is a standard template for a function (if you are not familiar with functions in PostgreSQL, <a href=https://learn.crunchydata.com/postgresql-devel/courses/beyond-basics/basicfunctions>here is</a> a nice tutorial). Here is the <a href=https://github.com/thesteve0/postgres-fire/blob/master/sql/5_logistic_model.sql#L9>complete code</a> for my embedded function. In this blog post I am going to break it down by topical areas.<h3 id=function-signature><a href=#function-signature>Function Signature</a></h3><p>The function signature is a typical Postgres function signature.<pre><code class=language-pgsql>CREATE OR REPLACE FUNCTION final.logistic_model()
</code></pre><pre><code class=language-pgsql>RETURNS bytea as $$
</code></pre><ul><li>We create this function in the <code>final</code> schema, which is also where the data is located.<li>We are not passing in any data because we will get the table data through the R code.<li>We are going to return bytea because we are actually going to store the logistic model created in R into another table.</ul><h3 id=r-postgresql-connection-and-data-code><a href=#r-postgresql-connection-and-data-code>R PostgreSQL Connection and Data Code</a></h3><p>To begin our R function we need to get our data out of the table and into R. Remember, all of this connection and data loading code was just from me working in R on my desktop. The PL/R extension will actually “translate” all of these calls into direct db calls.<ol><li>We load the library for the R, RPostgreSQL, which will handle all the connections and queries.<li>The next two lines load the driver and make the connection to our Postgres instance.<li>The following line uses the connection (<code>con</code>) to do a SQL query against our database and put the results in a R dataframe (<code>df</code>).</ol><p>So once we make this into PL/R none of these connection calls actually use RPostgreSQL, they use the PL/R versions which are no-ops. And it executes the <code>dbGetQuery</code> against the current database.<h3 id=executing-r-code><a href=#executing-r-code>Executing R Code</a></h3><p>Other than the data query, our function only executes one line of R code. We use the standard way to do logistic regression in R.<pre><code class=language-r>logmodel_solar &#60- glm(hasfire ~ cs_rh_min + cs_air_max_temp + cs_precip + cs_solar, data=df, family = binomial("logit"))
</code></pre><ol><li>Use the glm function to carry out a <a href=https://en.wikipedia.org/wiki/Generalized_linear_model>generalized linear model</a>.<li><code>hasfire</code> is the dependent variable (coded as 1 for fire that day and 0 for no fire)<li>The dependent variable is going to be linearly related to 4 independent variables<li>Use the dataframe, <code>df</code>, as the source of all the data values<li>Use the link function of type <a href=https://en.wikipedia.org/wiki/Generalized_linear_model#Logit_link_function>binomial logit</a>. Using this link type changes the predicted values from any real number in ordinary linear regression, to the 1,0 in logistic regression.<li>Save the output into an object named <code>logmodel_solar</code>.</ol><p>Included in <code>logmodel_solar</code> is not only the significance of the overall model but the actual parameter values for each independent variable. This will be important later in the blog.<h3 id=cleaning-up-and-returning><a href=#cleaning-up-and-returning>Cleaning Up and Returning</a></h3><p>The next two lines release the connection to the database (a no-op in PL/R) and unload the driver. Finally, we return <code>logmodel_solar</code>. Now you may be wondering, what happens to an R object when we pass it back to PostgreSQL. The PL/R doc clearly lays out the translation between R and PostgreSQL <a href=https://github.com/postgres-plr/plr/blob/master/userguide.md#passing-data>data types</a>. In our case, since we are returning an R object, PL/R will convert that to a bytea type in PostgreSQL.<h2 id=how-to-use-the-function><a href=#how-to-use-the-function>How to Use the Function</a></h2><p>With our spiffy function in hand, how do we use it? Well it’s as simple as a SQL call. We are actually going to combine running the function with saving its model to a table. Our table for holding the function output only has the following structure:<p><code>final.logistic_models</code><ol><li><code>id</code>: self-incrementing integer column to act as the primary key<li><code>name</code>: text field that can hold a name for the model<li><code>model</code>: bytea column to hold the model object<li><code>created_at</code>: timestamp column which defaults to now()</ol><p>So running our logistic regression model and creating a new table record is as simple as:<pre><code class=language-pgsql>INSERT INTO final.logistic_models (name, model) VALUES ('first run', final.logistic_model());
</code></pre><p>With that in place, we could actually run a job every week to refit the logistic regression model based on newly input data. As a data scientist I can pull the more recent model back out of the database and look at the model object in R on my desktop. I could do this without even having access to the original data.<h2 id=wrap-up><a href=#wrap-up>Wrap Up</a></h2><p>In today’s post we demonstrated how a data scientist can use R with PostgreSQL on their desktop and server-side. The great part was my R code required 0 changes to work in PL/R.<p>This scenario works well for a company that wants the data scientist working on the statistical algorithm and then, when they finished, expose it in a “safe” way to anyone using the database. By embedding it in PostgreSQL you get four immediate benefits:<ol><li>The data scientist's R code doesn’t have to be translated to a language that the DBA or application developer understands.<li>Since the code is running on server with the formula and where the data is stored, your program avoids a whole round trip over the network to do the data analysis.<li>You get a nice clear separation of concerns; the data scientist gets to build and tune the statistical algorithm and the consumer of the output has no way to alter them.<li>As long as your functions are ready only, with Postgres read replicas, you get easy horizontal scaling of your algorithm. If you want to give it a try, setting up read replicas is trivial to do on a platform like <a href=https://www.crunchydata.com/products/crunchy-bridge>Crunchy Bridge</a> (which also has PL/R installed).</ol><p>If you are wondering how an application developer can take advantage of this, tune into the final blog post in the series.<p>What do you have to say about this? Do you use R in your daily work? Have you ever tried PL/R to do your work? I would love to hear about your experience, share it with us on <a href=https://twitter.com/crunchydata>Twitter</a>. Happy coding and data sciencing! ]]></content:encoded>
<category><![CDATA[ Fun with SQL ]]></category>
<author><![CDATA[ Steve.Pousty@crunchydata.com (Steve Pousty) ]]></author>
<dc:creator><![CDATA[ Steve Pousty ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/using-r-in-postgres-for-logistic-regression-modeling-1</guid>
<pubDate>Wed, 06 Jan 2021 04:00:00 EST</pubDate>
<dc:date>2021-01-06T09:00:00.000Z</dc:date>
<atom:updated>2021-01-06T09:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Using PL/pgSQL to Calculate New Postgres Columns ]]></title>
<link>https://www.crunchydata.com/blog/using-plpgsql-to-calculate-new-postgres-columns</link>
<description><![CDATA[ In our last blog post on using Postgres for statistics, I covered some of the decisions on how to handle calculated columns in PostgreSQL. I chose to go with adding extra columns to the same table and inserting the calculated values into these new columns. Today’s post is going to cover how to implement this solution using PL/pgSQL. ]]></description>
<content:encoded><![CDATA[ <p>In our last blog post on <a href=/blog/using-postgresql-for-statistics-centering-and-standardizing-data>using Postgres for statistics</a>, I covered some of the decisions on how to handle calculated columns in <a href=/blog/postgres-the-batteries-included-database>PostgreSQL</a>. I chose to go with adding extra columns to the same table and inserting the calculated values into these new columns. Today’s post is going to cover how to implement this solution using <a href=https://www.postgresql.org/docs/current/plpgsql.html>PL/pgSQL</a>.<h2 id=why-plpgsql><a href=#why-plpgsql>Why PL/pgSQL?</a></h2><p>I bet your first question is going to be: Why did you choose to do this in PL/pgSQL rather than just some quick editor work?<p>I actually started with handcrafting all the <dfn>data definition language</dfn> (<a href=https://www.postgresql.org/docs/current/ddl.html><abbr>DDL</abbr></a>) and SQL statements and quickly changed my mind. Just to remind you, here are all the explanatory columns we have in our fire dataset:<p><img alt="a psql output showing the structure of the final.analsyis table"loading=lazy src=https://f.hubspotusercontent00.net/hubfs/2283855/Google%20Drive%20Integration/Using%20PlpgSQL%20to%20Calculate%20New%20Postgres%20Columns.png><p>Hasfire is the variable we are trying <a href=/blog/1-using-postgresql-to-shape-and-prepare-scientific-data>to predict</a> (the occurrence of a fire) and all the other columns are the potential explanatory variables.<h2 id=reasons-to-replace-copy-and-paste-with-code><a href=#reasons-to-replace-copy-and-paste-with-code>Reasons to Replace Copy and Paste with Code</a></h2><p>I tried to use code to make the new calculated columns because:<ol><li>Not only would I have to make 9 new table columns but also 9 different <code>INSERT</code> formula columns. All that is a great opportunity to put a typo in a column name or a formula. It is also a lot of <a href=https://softwareengineering.stackexchange.com/questions/87696/is-copy-paste-programming-bad>copy and pasting</a>.<li>Calculating Z-scores is a standard step in many data science workflows. By writing a PL/pgSQL function, I could simply repeat calculating columns in any new analysis by using the function. It would also be available to other data scientists to use as well.<li>I need to learn more PL/pgSQL and this seemed like an interesting problem. The function was going to have to run some DDL and some SQL.</ol><h2 id=design-considerations><a href=#design-considerations>Design Considerations</a></h2><p>As mentioned above, I wanted the function to be generic so I could re-use it in other places. I also decided that it would be best to retain the original column name in the calculated column name. Finally, I wanted to handle all the columns at once, not a function for a single column at a time.<h2 id=implementation><a href=#implementation>Implementation</a></h2><p>With these constraints in mind, I came up with a function that accomplishes all 3 goals. To see the entire function, it's <a href=https://github.com/thesteve0/postgres-fire/blob/master/sql/4-centerstandardize.sql#L11>on github</a>, but for the blog post I am going to break it down into sections.<h2 id=postgresql-function-signature><a href=#postgresql-function-signature>PostgreSQL Function Signature</a></h2><p>Let’s start with the function signature:<pre><code class=language-pgsql>CREATE OR REPLACE FUNCTION final.initial_center_standardize(schema_name TEXT, table_name TEXT, prefix TEXT, column_names TEXT\[\], pkey TEXT) RETURNS TEXT as
</code></pre><p>The beginning of the statement is just normal SQL to create a new function. Notice I am actually creating this function inside the schema name “final”. The first two parameters get the schema and table name for the table where we are going to add the columns.<p>The next parameter is going to be the prefix for the new calculated column name. The next parameter is an array of text values (which is a data type in PostgreSQL). The array contains the name of all the columns for which we want Z-scores. Finally, we need the name of the primary key column for one of the queries used in the function.<p>I just return a text field because none of this work will return any data values, but I like to show the caller if the call worked. I usually return “done” or “finished” for success and “error” with some text explaining what went wrong.<p>Just to be clear, for those new to PL/pgSQL programming, I did not come up with this signature at the beginning. Some of the needed parameters only became apparent as I built the rest of the function.<h2 id=loop-with-iterators-in-plpgsql><a href=#loop-with-iterators-in-plpgsql>Loop with Iterators in PL/pgSQL</a></h2><p>With the signature done, we first declare some variables we will need later in the function. PL/pgSQL <a href=https://www.postgresql.org/docs/13/plpgsql-declarations.html>requires</a> you to pre-declare any variables you use in a block.<p>The next step is to create a <a href=https://www.postgresql.org/docs/current/plpgsql-control-structures.html#PLPGSQL-FOREACH-ARRAY><code>foreach</code> loop</a> to give us access to each individual element in the array of column names we passed in.<pre><code class=language-pgsql>FOREACH col IN ARRAY column_names
    LOOP
</code></pre><p>Like a normal looping and <a href=https://openbookproject.net/thinkcs/python/english3e/iteration.html>iterator pattern</a>, this code will extract the first element in the array and give that element the name ‘col’ in the body of the loop. It will continue to iterate through array elements until there are no more elements in the array. This is perfect for our work, each column is going to get the same treatment.<h2 id=creating-the-new-columns><a href=#creating-the-new-columns>Creating the New Columns</a></h2><p>Next our code takes on the task of creating the new column to hold the Z-score. First we create the new column name and storing it in a variable:<pre><code class=language-pgsql>new_col_name := prefix || col;
</code></pre><p>We take the prefix passed in and concatenate it ("<code>||</code>" in SQL) to the original column name. I like using the same prefix before each column because it makes it clearer which columns are the raw columns and which have the Z-score.<p>Now we do our actual SQL to create the new column in the same table:<pre><code class=language-pgsql>EXECUTE FORMAT ('ALTER TABLE %I.%I ADD COLUMN %I NUMERIC', schema_name, table_name, new_col_name);
</code></pre><p>Normally PL/pgSQL will cache the query plan for a SQL statement. In our case, this statement is going to change each time we run through the loop, making our query dynamic. For dynamic queries you use <a href=https://www.postgresql.org/docs/13/plpgsql-statements.html#PLPGSQL-STATEMENTS-EXECUTING-DYN><code>EXECUTE</code></a> to tell the PostgreSQL query planner not to cache the query.<p>To make sure we do SQL safe variable substitution, we use the <a href=https://www.postgresql.org/docs/13/functions-string.html#FUNCTIONS-STRING-FORMAT><code>FORMAT</code></a> command. The <code>%I</code> in the SQL statement indicates we need this value to be treated like a SQL identifier (i.e. put <code>"</code> around a capitalized table name or escape an <code>'</code> in a string value). This is the same behavior as the <a href=https://www.postgresql.org/docs/current/functions-string.html><code>quote_ident</code></a> command.<p>Then, by default, the parameters are substituted into the string in the order they appear at the end of the <code>FORMAT</code> command. So if:<ul><li>schema_name = final<li>table_name = analysis<li>new_column_name = z_precip</ul><p>Then the following statement will be executed:<pre><code class=language-pgsql>ALTER TABLE final.analysis ADD COLUMN z_precip NUMERIC
</code></pre><p>In the specific case of our example we can get away with making every column <code>NUMERIC</code> because Z-scores are always a decimal number regardless of the number type of the input. And with that, we have created our new column in the table.<h2 id=populating-the-data-values-using-plpgsql><a href=#populating-the-data-values-using-plpgsql>Populating the Data Values using PL/pgSQL</a></h2><p>We have a new column but there is no data in it. Let’s solve that now. Here is the statement I used to calculate the Z-score for the new column (formatted for readability):<pre><code class=language-pgsql>EXECUTE FORMAT(
'WITH summary AS ( SELECT AVG(%1$I) AS avg, STDDEV(%1$I) AS stddev FROM %3$I.%4$I), ' ||
'final_select AS (SELECT %5$I, (%1$I - avg)/stddev AS centered FROM %3$I.%4$I CROSS JOIN summary) ' ||
UPDATE %3$I.%4$I SET %2$I = final_select.centered FROM final_select WHERE final_select.%5$I = %3$I.%4$I.%5$I ', col, new_col_name, schema_name, table_name, pkey);
</code></pre><p>Again we use the <code>EXECUTE FORMAT</code> so the planner doesn’t cache the query and we pass in new variables with each iteration of the loop. The actual SQL has two <a href=https://www.postgresql.org/docs/current/queries-with.html>CTE</a>s to calculate the Z-score and then an UPDATE statement to insert the Z-score into the right table and column.<p>Because we are reusing parameters in the SQL, we have to use a slightly different syntax for the variable substitution (highlighted in orange). Now we have variables like <code>%1$I</code> rather than just <code>%I</code>. This new notation means take the first parameter from all the parameters passed in to <code>FORMAT</code>. If we had written the query the previous way, we would have passed in the same parameters multiple times in the function call. This numbered reference to parameter format is much cleaner.<p>The first CTE (<code>summary</code>) calculates the average and standard deviation for the current column in the loop. We take those values and use them in the second CTE (<code>final_select</code>). By using the <code>CROSS JOIN</code> in this query we can use the <code>avg</code> and <code>stddev</code> in the Z-score calculation for each row in the data set. The final query is just updating the new column table with the Z-score where <code>pkey</code> in the <code>final_select</code> matches the <code>pkey</code> of the table.<h2 id=closing-out-a-plpgsql-function><a href=#closing-out-a-plpgsql-function>Closing Out a PL/pgSQL Function</a></h2><p>The creation of a new column and populating the values continues for each item in the iteration loop. We terminate the loop with the <code>END LOOP</code> statement. We then return ‘done’ to the SQL call that used the function. Again, I like returning this string because it allows the function user to know if the function actually finished, or it is taking a long time. You could change this to return NULL if you want.<h2 id=wrap-up><a href=#wrap-up>Wrap Up</a></h2><p>There you have it, my cool little function. I know it may have looked complicated at first glance, but now that we are finished you can see there are only 2 SQL lines to carry out all our work. Think of how much copy and pasting this saved!<p>The other big benefit is now I can reuse this function in any database in the future. I can control access to it by using schema permissions. As a matter of fact, I could add this function as part of a template. Then every database created in our hypothetical data science PostgreSQL cluster would have access to this function. I could give data scientists with permission to make this call and quickly calculate Z-scores without even having to know all the PL/pgSQL.<p>Finally this example could easily be modified to create a new table with new columns for the Z-scores. It could also be quickly modified to use an auxiliary table where the average and standard deviation for each predictive variable is stored. This was one of the possible solutions I explained in the previous post that would probably help overall performance.<p>I hope you learned something new in this post. Maybe you have already done some PL/pgSQL to alter tables. What kind of use cases did you have? What are something of the interesting twists you uncovered. I would love to hear your thoughts, please sent them to the Crunchy Data <a href=https://twitter.com/crunchydata>twitter account</a>. Happy function writing! ]]></content:encoded>
<category><![CDATA[ Fun with SQL ]]></category>
<author><![CDATA[ Steve.Pousty@crunchydata.com (Steve Pousty) ]]></author>
<dc:creator><![CDATA[ Steve Pousty ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/using-plpgsql-to-calculate-new-postgres-columns</guid>
<pubDate>Thu, 10 Dec 2020 04:00:00 EST</pubDate>
<dc:date>2020-12-10T09:00:00.000Z</dc:date>
<atom:updated>2020-12-10T09:00:00.000Z</atom:updated></item>
<item><title><![CDATA[ Replacing Lines of Code with 2 Little Regexs in Postgres ]]></title>
<link>https://www.crunchydata.com/blog/replacing-lines-of-code-with-2-little-regexs-in-postgresql</link>
<description><![CDATA[ For those of you who have a bad taste in your mouth from earlier run-ins with regexs, this will be more use case focused and I will do my best to explain the search patterns I used. ]]></description>
<content:encoded><![CDATA[ <p>Greetings readers, today we're going to take a semi-break from my “doing <a href=/blog/1-using-postgresql-to-shape-and-prepare-scientific-data>data science</a> in <a href=https://www.crunchydata.com/blog/randomly-sampling-data-using-sql-and-postgresql>SQL</a>” series to cover a really cool use case I just solved with <a href=https://en.wikipedia.org/wiki/Regular_expression><dfn>regular expressions</dfn></a> (<abbr>regex</abbr>) <a href=https://www.postgresql.org/docs/current/functions-matching.html>in Postgres</a>. For those of you who have a bad taste in your mouth from earlier run-ins with regexs, this will be more use case focused and I will do my best to explain the search patterns I used.<p>If you've never heard of regex, there are <a href=https://regexone.com/>good</a> resources to <a href=https://www.oreilly.com/content/an-introduction-to-regular-expressions/>learn</a> more about them but I will not be giving <a href=https://www.rexegg.com/>a tutorial</a>. I highly recommend learning about them because:<ol><li>They are available in almost all modern programming languages<li>They help you solve problems that would be really awkward in code<li>Most editors also let use regex for find/replace</ol><p>I have loved regexs ever since I met them. I find them highly logical and easier than writing a bunch of code. I know not all devs <a href=https://stackoverflow.com/questions/764247/why-are-regular-expressions-so-controversial>agree with me</a>, but to each their own.<h2 id=background><a href=#background>Background</a></h2><p>I have been doing some work with <a href=https://www.islandconservation.org/>Island Conservation</a> to help them manage data. I am moving their data off different systems into one centralized PostgreSQL database (with the <a href=https://postgis.net/>PostGIS</a> extension as well).<p>They are doing a study trying to use contraceptives, as opposed to kill trapping, for rodent population management. One of the devices they were using in the field was a camera trap (otherwise known as a game camera).<p>While the field collector (software on a phone) was giving each camera a <a href=https://en.wikipedia.org/wiki/Universally_unique_identifier>GUID</a> for the primary key, the humans would always use the camera name to identify it. Unfortunately, the software was not using controlled vocabularies or value checking for the camera name on the client. All the data values were extracted from the data collector website in <a href=https://geojson.org/>GeoJSON</a> format. It was then converted to PostGIS SQL insert statements using <a href=https://gdal.org/programs/ogr2ogr.html>ogr2ogr</a> and imported into a table.<h2 id=the-problem><a href=#the-problem>The Problem</a></h2><p>Now is when things get interesting. Because they were not using a controlled vocabulary, or even formatting requirements on the camera name field, we got all sorts of values there. Some just had “testing” strings in their names and we could ignore them. But others were supposed to follow the format “camXXX” where XXX is 3 digits. The XXX would start with 001 for the first station and then increase accordingly for new stations measured.<p>Here are just some of the different variations I saw:<table><thead><tr><th>Value in Table for camera name<th>Should be<tbody><tr><td>cam058<td>This is correct<tr><td>cam08<td>cam008<tr><td>cam59<td>cam059<tr><td>CAM011<td>cam011</table><p>The examples above should demonstrate that there are two classes of problems in the data:<ol><li>Camera names are missing a 0 preceding some of numeric part of the name where the digits are less than 100<li>The letters in cam are capitalized and should be lowercase</ol><h2 id=the-solution><a href=#the-solution>The Solution</a></h2><p>Now my first instinct was to say “time to break out some Python and process the original file and fix this”. But my new response kicked in and I said, “Let’s see if we can do this in the database with SQL!”<p>I start searching the PostgreSQL doc and look what <a href=https://www.postgresql.org/docs/current/functions-matching.html>I find</a>! If you search the page you can find <code>regexp_replace</code> which we'll use today (by the way, this doc page should really list all the <code>regexp_*</code> functions). What this commands does is:<ol><li>Take an input string<li>Test the regex against the input string<li>If the regex returns true (the pattern is found), return the replacement string<li>If it returns false, return the original input string</ol><p>Ok,, so that fits our requirements quite nicely, time to get writing the regex. Let me be clear again, I am not going to teach you regex today but I will try to walk you through the patterns I used.<h2 id=fixing-the-missing-0s><a href=#fixing-the-missing-0s>Fixing the missing 0s</a></h2><p>Alright let’s put one of the incorrect strings here first:<pre><code class=language-txt>cam08
</code></pre><p>Here is the SQL I used to fix it<pre><code class=language-pgsql>UPDATE camera SET cameraname = regexp_replace(cameraname, 'cam(\d{2})\s\*$',
'cam0\1' )
</code></pre><p>The SQL parts of this should look like a straightforward UPDATE statement. Let’s look at the <code>regexp_replace</code> function call. The first parameter is the field we want to search, the second parameter is the regex we are using for the test, and the last parameter is the substitution phrase.<p>Here is the regex by itself:<pre><code class=language-regex>‘cam(\d{2})\s\*$’
</code></pre><p>Breaking it down in English:<ol><li>Look for the string “cam” - capitalization matters in regex unless you tell the command to ignore case.<li>Now the <code>()</code> means we are going to want to keep whatever is matched inside the parentheses for later<li><code>\d</code> means a digit character (0-9)<li><code>{2}</code> means we want exactly 2 digits<li><code>\s</code> means a space character<li><code>-</code> means 0 or more space characters<li><code>$</code> means anchor this pattern at the end of the string (this whole pattern has to occur at the end of the string)</ol><p>Let me write this as sentences:<p>At the end of the string look for “cam” followed by exactly 2 digits, save the two digits for later, and there may be trailing spaces.<p>This exactly matches our records with only 2 numbers rather 3. I also discovered the “hidden” spaces while doing this so I had to put the \s on the end.<p>I had to anchor to the end so that we wouldn’t match cam001. This anchor is needed because cam00, in cam001, would also match “cam followed by exactly 2 digits followed by 0 or more spaces”.<p>Now when we look at the third parameter, the replacement string it should look pretty straight forward except the<pre><code class=language-txt>cam0\1
</code></pre><p>This says put “cam0” at the beginning of the replacement string. For the \1 we have to go back to step 2 above. That group we put in the () was the first group we captured in the regex. So now \1 contains the content of the first group, which in our case was the 2 digits. If we had another () in the regex that would have been \2.<p>With this one call we replace all the incorrect camXX values because when the regex matches we get the new string and when it doesn’t we just set the original string back to itself. Boom!<h2 id=fixing-the-capital-letters><a href=#fixing-the-capital-letters>Fixing the Capital Letters</a></h2><p>With that other regex under our belt this one should be easier to understand. If you didn’t understand the other one maybe this will help you understand it more. Again, here is our problem string:<pre><code class=language-txt>CAM011
</code></pre><p>Again we are going to use regex replace. Here is the regex expression I came up with:<pre><code class=language-pgsql>UPDATE camera SET cameraname = regexp_replace(cameraname,
'^CAM(\d{3})\s\*$', 'cam\1')
</code></pre><p>Here is the regex by itself:<pre><code class=language-regex>'^CAM(\d{3})\s\*$'
</code></pre><p>Breaking it down in English:<ol><li><code>^</code> means anchor/start at the beginning of the line<li>Look for the string “CAM”<li><code>()</code> means we are going to want to save the contents inside<li><code>\d</code> means means a digit (0-9)<li><code>{3}</code> means we want exactly 3 digits<li><code>\s</code> means a space<li><code>-</code> means 0 or more spaces<li><code>$</code> means this pattern goes to the end of the line</ol><p>The sentence for this one looks like:<p>The beginning of the line has to start with “CAM”, followed by exactly 3 digits that we save for later, then 0 or more spaces, and finally this string has to go to the end of the line.<p>I don’t want to pick up a string like “This is CAM000 test”. To make sure our pattern starts at the beginning of the line, we use <code>^</code>. We also anchor at the end of the line to ensure the pattern we are searching for is the whole line.<p>And the replacement parameter is this:<pre><code class=language-txt>'cam\1'
</code></pre><p>Since we captured 3 digits in our parentheses, that capture can go right after the ‘cam’ string to make a valid camera name.<h2 id=wrap-up><a href=#wrap-up>Wrap Up</a></h2><p>I hope you see both the power and the ease of using regular expressions in the database. Just like programming languages, you can start with simple statements and grow in your expertise over time. There are also some nice <a href=https://regex101.com/>web applications</a> to play around and test your regex.<p>As I said before, you can also use regular expressions in so <a href=https://programminghistorian.org/en/lessons/understanding-regular-expressions>many other places</a>, like your programming language of choice or your favorite IDE/editor. Do you love regex as much as me? Do you have any cool examples of using them in Postgres? If so shoot us a message on our <a href=https://twitter.com/crunchydata>twitter account</a>. Happy pattern matching! ]]></content:encoded>
<category><![CDATA[ Fun with SQL ]]></category>
<author><![CDATA[ Steve.Pousty@crunchydata.com (Steve Pousty) ]]></author>
<dc:creator><![CDATA[ Steve Pousty ]]></dc:creator>
<guid isPermalink="false">https://blog.crunchydata.com/blog/replacing-lines-of-code-with-2-little-regexs-in-postgresql</guid>
<pubDate>Wed, 02 Dec 2020 04:00:00 EST</pubDate>
<dc:date>2020-12-02T09:00:00.000Z</dc:date>
<atom:updated>2020-12-02T09:00:00.000Z</atom:updated></item></channel></rss>