<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>SQL on R Views</title>
    <link>https://rviews.rstudio.com/tags/sql/</link>
    <description>Recent content in SQL on R Views</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 06 Apr 2023 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://rviews.rstudio.com/tags/sql/" rel="self" type="application/rss+xml" />
    
    
    
    
    <item>
      <title>A data analyst workflow, part 1: SQL &amp; tidyverse</title>
      <link>https://rviews.rstudio.com/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/</link>
      <pubDate>Thu, 06 Apr 2023 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/</guid>
      <description>
        


&lt;p&gt;&lt;em&gt;Vidisha Vachharajani works in the EdTech industry, where she enjoys developing data-driven strategy solutions for learners. She has been an R user for over 15 years.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;As a data professional, I have enjoyed learning and using multiple tools for my workflows. For me, everything used to begin and end with R. Today, SQL is a must-know. Not being able to pull your own custom tables from a warehouse can make things tricky. Then there is &lt;code&gt;tidyverse&lt;/code&gt;, the master collection of packages for data science &amp;amp; analytics. As an OG R user, I cannot envision data work without &lt;code&gt;tidyverse&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In this first part of a 2-part article, I want to demonstrate how a data analyst can use &lt;em&gt;one OR the other for the initial stages of data exploration&lt;/em&gt;, and then double down on &lt;code&gt;tidyverse&lt;/code&gt;, leveraging &lt;code&gt;ggplot2&lt;/code&gt; for a deeper exploration. By no means does this preclude the extensive use of SQL for data wrangling. Rather, this post showcases the wonders of &lt;code&gt;tidyverse&lt;/code&gt; (a &lt;a href=&#34;https://www.tidyverse.org/&#34;&gt;collection&lt;/a&gt; of R packages designed for data science, sharing an underlying design philosophy, grammar, and data structures) and specifically, &lt;code&gt;ggplot2&lt;/code&gt; (the &lt;a href=&#34;https://ggplot2-book.org/&#34;&gt;language&lt;/a&gt; of elegant graphics) for a SQL user’s benefit.&lt;/p&gt;
&lt;div id=&#34;the-dataset-and-the-goal&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;1. The dataset and the goal&lt;/h2&gt;
&lt;p&gt;The dataset I am using is clinical. Sourced from the UCI machine learning repo, it is the &lt;em&gt;Diabetes 130-US hospitals for years 1999-2008 Data Set&lt;/em&gt;. The dataset is large, ~100K rows and 51 columns in its raw format. It is, however, clean data. For the purpose of this article, in order to show SQL and &lt;code&gt;tidyverse&lt;/code&gt; language in tandem, I will split it up into 5 parts, and we will assume that the data is actually available to us in these 5 different pieces, rather than as the whole, cleaned data, since this is typically the case in real life.&lt;/p&gt;
&lt;p&gt;I will skip the portion about &lt;a href=&#34;https://dbplyr.tidyverse.org/articles/dbplyr.html&#34;&gt;&lt;code&gt;dbplyr&lt;/code&gt;&lt;/a&gt;, referring readers to the hyperlinked article that will show you how to actually pull data from a remote database using &lt;code&gt;tidyverse&lt;/code&gt;’s &lt;code&gt;dbplyr&lt;/code&gt;. Typically, this is done using SQL, but&lt;code&gt;dbplyr&lt;/code&gt; allows you to do this within &lt;code&gt;R&lt;/code&gt;. Rather, I will focus on &lt;em&gt;the initial stages of data exploration&lt;/em&gt;, using both SQL and &lt;code&gt;tidyverse&lt;/code&gt; for the same output, while extending the &lt;code&gt;tidyverse&lt;/code&gt; portion to include &lt;code&gt;ggplot2&lt;/code&gt; visualization examples, using different plot types for each use case. Note that in each case, you can use SQL first, and then use the SQL output as an input for the &lt;code&gt;ggplot2&lt;/code&gt; visualization.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;reading-in-the-data&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;2. Reading in the data&lt;/h2&gt;
&lt;p&gt;The data has been split into 5 parts – demographic, medical, hospital visits, outcome, test results. To learn more about the actual data, see &lt;a href=&#34;https://www.hindawi.com/journals/bmri/2014/781670/&#34;&gt;here&lt;/a&gt;. Each part is connected with the other through a UID that is a concatenation of the patient encounter ID and the patient number (using either one doesn’t work to make the ID unique). Note that all of the analyses in this post will be done at the UID level, rather than patient level.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#rm(list=ls())
library(sqldf)
library(dplyr)
library(readxl)
#library(dbplyr)
library(ggplot2)

data_path &amp;lt;- &amp;quot;./dataset_diabetes/diabetic_data.xlsx&amp;quot;
dem &amp;lt;- read_excel(data_path, &amp;quot;demo&amp;quot;)
meds &amp;lt;- read_excel(data_path, &amp;quot;medications&amp;quot;)
visits &amp;lt;- read_excel(data_path, &amp;quot;hosp_visits&amp;quot;)
y &amp;lt;- read_excel(data_path, &amp;quot;readmissions&amp;quot;)
results &amp;lt;- read_excel(data_path, &amp;quot;test_results&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;early-explorations&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;3. Early explorations&lt;/h2&gt;
&lt;p&gt;Let’s begin using SQL and &lt;code&gt;tidyverse&lt;/code&gt; to answer some initial questions related to the dataset. The primary hypothesis for this data is the &lt;strong&gt;impact of HbA1c measurement on readmission rates&lt;/strong&gt;, where “readmission” is our response. We will also answer a number of other questions along the way to understand the data better, using &lt;code&gt;ggplot2&lt;/code&gt; when we can.&lt;/p&gt;
&lt;div id=&#34;look-at-the-data&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;3.1 Look at the data&lt;/h3&gt;
&lt;div id=&#34;get-some-counts&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.1.1 Get some counts&lt;/h4&gt;
&lt;p&gt;Let’s take a look at medications and get a sample size for it, first using SQL and then &lt;code&gt;R&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT * FROM meds where 1=0&amp;#39;) # SQL see col names&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##  [1] uid                      metformin                repaglinide             
##  [4] nateglinide              chlorpropamide           glimepiride             
##  [7] acetohexamide            glipizide                glyburide               
## [10] tolbutamide              pioglitazone             rosiglitazone           
## [13] acarbose                 miglitol                 troglitazone            
## [16] tolazamide               examide                  citoglipton             
## [19] insulin                  glyburide-metformin      glipizide-metformin     
## [22] glimepiride-pioglitazone metformin-rosiglitazone  metformin-pioglitazone  
## [25] change                   diabetesMed             
## &amp;lt;0 rows&amp;gt; (or 0-length row.names)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT uid, metformin, repaglinide, nateglinide, chlorpropamide FROM meds LIMIT 5&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##               uid metformin repaglinide nateglinide chlorpropamide
## 1 2278392-8222157        No          No          No             No
## 2 149190-55629189        No          No          No             No
## 3  64410-86047875        No          No          No             No
## 4 500364-82442376        No          No          No             No
## 5  16680-42519267        No          No          No             No&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;head(meds, n=5) # dplyr&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## # A tibble: 5 × 26
##   uid    metfo…¹ repag…² nateg…³ chlor…⁴ glime…⁵ aceto…⁶ glipi…⁷ glybu…⁸ tolbu…⁹
##   &amp;lt;chr&amp;gt;  &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;  
## 1 22783… No      No      No      No      No      No      No      No      No     
## 2 14919… No      No      No      No      No      No      No      No      No     
## 3 64410… No      No      No      No      No      No      Steady  No      No     
## 4 50036… No      No      No      No      No      No      No      No      No     
## 5 16680… No      No      No      No      No      No      Steady  No      No     
## # … with 16 more variables: pioglitazone &amp;lt;chr&amp;gt;, rosiglitazone &amp;lt;chr&amp;gt;,
## #   acarbose &amp;lt;chr&amp;gt;, miglitol &amp;lt;chr&amp;gt;, troglitazone &amp;lt;chr&amp;gt;, tolazamide &amp;lt;chr&amp;gt;,
## #   examide &amp;lt;chr&amp;gt;, citoglipton &amp;lt;chr&amp;gt;, insulin &amp;lt;chr&amp;gt;,
## #   `glyburide-metformin` &amp;lt;chr&amp;gt;, `glipizide-metformin` &amp;lt;chr&amp;gt;,
## #   `glimepiride-pioglitazone` &amp;lt;chr&amp;gt;, `metformin-rosiglitazone` &amp;lt;chr&amp;gt;,
## #   `metformin-pioglitazone` &amp;lt;chr&amp;gt;, change &amp;lt;chr&amp;gt;, diabetesMed &amp;lt;chr&amp;gt;, and
## #   abbreviated variable names ¹​metformin, ²​repaglinide, ³​nateglinide, …&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT COUNT(uid) FROM meds&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##   COUNT(uid)
## 1     101766&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;nrow(meds) # R&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## [1] 101766&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How many patients with a diabetes diagnosis, vs respiratory, circulatory, etc.?&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT primary_diag, COUNT(*) FROM results GROUP BY primary_diag&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##   primary_diag COUNT(*)
## 1  circulatory    30437
## 2     diabetes     8757
## 3        other    48149
## 4  respiratory    14423&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;results %&amp;gt;% group_by(primary_diag) %&amp;gt;% count(primary_diag) # R&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## # A tibble: 4 × 2
## # Groups:   primary_diag [4]
##   primary_diag     n
##   &amp;lt;chr&amp;gt;        &amp;lt;int&amp;gt;
## 1 circulatory  30437
## 2 diabetes      8757
## 3 other        48149
## 4 respiratory  14423&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How many women came in through an emergency admission type?&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT gender, admission_type_id, COUNT(*) AS n FROM dem LEFT JOIN visits USING(uid) WHERE admission_type_id=1 GROUP BY gender&amp;#39;)  # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##            gender admission_type_id     n
## 1          Female                 1 29448
## 2            Male                 1 24540
## 3 Unknown/Invalid                 1     2&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;visits %&amp;gt;% left_join(dem, by=join_by(uid)) %&amp;gt;% subset(admission_type_id==1) %&amp;gt;% count(gender, admission_type_id) # dplyr&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## # A tibble: 3 × 3
##   gender          admission_type_id     n
##   &amp;lt;chr&amp;gt;                       &amp;lt;dbl&amp;gt; &amp;lt;int&amp;gt;
## 1 Female                          1 29448
## 2 Male                            1 24540
## 3 Unknown/Invalid                 1     2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;a-mosaic-plot&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.1.2 A mosaic plot&lt;/h4&gt;
&lt;p&gt;Instead of extracting counts manually, let’s use a mosaic plot to get a sense of how 2 count variables are distributed relative to each other. In this case, age and admission type. This plot sheds light into data availability and asymmetric distributions. For example, here, we see that most patients come from emergency, urgent care, or as an elective, and that there is missing or “not available” admission type data. It is important to retain these 2 categories separately, since they mean different things. Note that in the &lt;code&gt;ggplot&lt;/code&gt; parameters, I have not yet introduced axes label cleanup, etc.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;p0 &amp;lt;- dem %&amp;gt;% left_join(visits, by=join_by(uid)) %&amp;gt;%  
  mutate(admission_type=ifelse(admission_type_id==1, &amp;quot;1:Emergency&amp;quot;, 
                               ifelse(admission_type_id==2, &amp;quot;2:Urgent&amp;quot;, 
                               ifelse(admission_type_id==3, &amp;quot;3:Elective&amp;quot;, 
                               ifelse(admission_type_id==4, &amp;quot;4:Newborn&amp;quot;, 
                               ifelse(admission_type_id==5, &amp;quot;5:Not Available&amp;quot;,
                               ifelse(admission_type_id==6, &amp;quot;6:NULL&amp;quot;, 
                               ifelse(admission_type_id==7, &amp;quot;7:Trauma Center&amp;quot;, 
                                      &amp;quot;8:Not Mapped&amp;quot;)))))))) %&amp;gt;% 
  group_by(admission_type, age) %&amp;gt;% summarise(n=n()) %&amp;gt;% mutate(freq = n / sum(n)) 
ggplot(p0, aes(x=age, y=admission_type)) +
  geom_tile(aes(fill=n)) + scale_fill_gradient(low=&amp;quot;white&amp;quot;, high=&amp;quot;blue&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/mosaic-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;a-simple-join&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.1.3 A simple join&lt;/h4&gt;
&lt;p&gt;Let’s join all 5 datasets and look at it. Note that in SQL, in order to look only at the first few columns, we need to know the column names, which is what we first do here.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# (Output suppressed)
sqldf(&amp;#39;SELECT * FROM dem LEFT JOIN visits USING(uid) LEFT JOIN results USING(uid) LEFT JOIN meds USING(uid) LEFT JOIN y USING(uid) where 1=0&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sqldf(&amp;#39;SELECT uid, race, gender, age, weight FROM dem LEFT JOIN visits USING(uid) LEFT JOIN results USING(uid) LEFT JOIN meds USING(uid) LEFT JOIN y USING(uid) LIMIT 5&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##               uid            race gender     age weight
## 1 2278392-8222157       Caucasian Female  [0-10)      ?
## 2 149190-55629189       Caucasian Female [10-20)      ?
## 3  64410-86047875 AfricanAmerican Female [20-30)      ?
## 4 500364-82442376       Caucasian   Male [30-40)      ?
## 5  16680-42519267       Caucasian   Male [40-50)      ?&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;dem %&amp;gt;% left_join(visits, by=join_by(uid)) %&amp;gt;% left_join(results, by=join_by(uid)) %&amp;gt;% left_join(meds, by=join_by(uid)) %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% print(n=5) # dplyr, by default shows 10 rows, so we ask it to print 5&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## # A tibble: 101,766 × 50
##   uid          race  gender age   weight admis…¹ disch…² admis…³ time_…⁴ payer…⁵
##   &amp;lt;chr&amp;gt;        &amp;lt;chr&amp;gt; &amp;lt;chr&amp;gt;  &amp;lt;chr&amp;gt; &amp;lt;chr&amp;gt;    &amp;lt;dbl&amp;gt;   &amp;lt;dbl&amp;gt;   &amp;lt;dbl&amp;gt;   &amp;lt;dbl&amp;gt; &amp;lt;chr&amp;gt;  
## 1 2278392-822… Cauc… Female [0-1… ?            6      25       1       1 ?      
## 2 149190-5562… Cauc… Female [10-… ?            1       1       7       3 ?      
## 3 64410-86047… Afri… Female [20-… ?            1       1       7       2 ?      
## 4 500364-8244… Cauc… Male   [30-… ?            1       1       7       2 ?      
## 5 16680-42519… Cauc… Male   [40-… ?            1       1       7       1 ?      
## # … with 101,761 more rows, 40 more variables: medical_specialty &amp;lt;chr&amp;gt;,
## #   num_lab_procedures &amp;lt;dbl&amp;gt;, num_procedures &amp;lt;dbl&amp;gt;, num_medications &amp;lt;dbl&amp;gt;,
## #   number_outpatient &amp;lt;dbl&amp;gt;, number_emergency &amp;lt;dbl&amp;gt;, number_inpatient &amp;lt;dbl&amp;gt;,
## #   diag_1 &amp;lt;chr&amp;gt;, diag_2 &amp;lt;chr&amp;gt;, diag_3 &amp;lt;chr&amp;gt;, number_diagnoses &amp;lt;dbl&amp;gt;,
## #   max_glu_serum &amp;lt;chr&amp;gt;, A1Cresult &amp;lt;chr&amp;gt;, primary_diag &amp;lt;chr&amp;gt;, metformin &amp;lt;chr&amp;gt;,
## #   repaglinide &amp;lt;chr&amp;gt;, nateglinide &amp;lt;chr&amp;gt;, chlorpropamide &amp;lt;chr&amp;gt;,
## #   glimepiride &amp;lt;chr&amp;gt;, acetohexamide &amp;lt;chr&amp;gt;, glipizide &amp;lt;chr&amp;gt;, glyburide &amp;lt;chr&amp;gt;, …&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;explore-the-response-readmissions&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;3.2 Explore the response: readmissions&lt;/h3&gt;
&lt;div id=&#34;lab-procedures&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.2.1 Lab procedures&lt;/h4&gt;
&lt;p&gt;Let’s start with the simplest question – for the primary response variable, “readmitted”, how many lab procedures were done by each category of the response? Note here that “number of lab procedures” is one of a handful of continuous design covariate – rest of the ~45 covariates are all categorical/discrete.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# How many lab tests performed for readmitted patients
sqldf(&amp;#39;SELECT readmitted, SUM(num_lab_procedures) AS n FROM visits LEFT JOIN y USING(uid) GROUP BY readmitted&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##   readmitted       n
## 1        &amp;lt;30  502275
## 2        &amp;gt;30 1558172
## 3         NO 2325224&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;visits %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% group_by(readmitted) %&amp;gt;% 
  summarise(n=sum(num_lab_procedures)) %&amp;gt;% mutate(freq = n / sum(n)) # dplyr, w/ an added proportion &lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## # A tibble: 3 × 3
##   readmitted       n  freq
##   &amp;lt;chr&amp;gt;        &amp;lt;dbl&amp;gt; &amp;lt;dbl&amp;gt;
## 1 &amp;lt;30         502275 0.115
## 2 &amp;gt;30        1558172 0.355
## 3 NO         2325224 0.530&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the above doesn’t really tell us much, other than actual counts, proportions by response categories, let’s use &lt;code&gt;ggplot2&lt;/code&gt; to explore the distribution of “number of lab procedures”, using a barplot/histogram approach, with “readmitted” as the &lt;code&gt;fill&lt;/code&gt; element. This helps us get a better picture of their relationship; we see here how, for a strikingly normally distributed “number of lab procedures” (other than 1 outlier), on average, the higher the volume of procedures, the more the proportion of readmitted.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# How many lab tests performed for readmitted patients, use ggplot2
p1 &amp;lt;- visits %&amp;gt;% left_join(y, by=join_by(uid))
ggplot(data = p1 ,aes(x=num_lab_procedures,fill=readmitted)) + geom_bar() + labs(x=&amp;quot;Number of lab procedures&amp;quot;, y=&amp;quot;counts&amp;quot;) + scale_y_continuous(
    labels = function(n) scales::comma(abs(n)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-1-2-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Let’s also do this using &lt;code&gt;ggplot&lt;/code&gt;’s beautiful density plots. It is a slightly different type of visual, and tells us how the distribution of X shifts left or right by the response or &lt;code&gt;fill&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;ggplot(p1, aes(num_lab_procedures)) + geom_density(aes(fill=factor(readmitted)), alpha=0.8) + labs(x=&amp;quot;Number of lab procedures&amp;quot;) &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-1-3-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;demographics&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.2.2 Demographics&lt;/h4&gt;
&lt;p&gt;Next, we ask how readmissions differ across age groups and gender. Let’s also plot this to understand the output better. We first use a population pyramid approach to get the counts and then barplot the proportions to get a better understanding of the variance in readmissions across these groups.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# What are readmission rates by the different age groups?
sqldf(&amp;#39;SELECT age, readmitted, COUNT(*) AS n FROM dem LEFT JOIN y USING(uid) GROUP BY age&amp;#39;)  # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##         age readmitted     n
## 1    [0-10)         NO   161
## 2   [10-20)        &amp;gt;30   691
## 3   [20-30)         NO  1657
## 4   [30-40)         NO  3775
## 5   [40-50)         NO  9685
## 6   [50-60)        &amp;gt;30 17256
## 7   [60-70)         NO 22483
## 8   [70-80)        &amp;gt;30 26068
## 9   [80-90)         NO 17197
## 10 [90-100)         NO  2793&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;p21 &amp;lt;- dem %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% group_by(gender, age, readmitted) %&amp;gt;% summarise(n=n()) %&amp;gt;%
mutate(pct = 100 * n / sum(n), readmission=ifelse(readmitted==&amp;quot;NO&amp;quot;, &amp;quot;not readmitted&amp;quot;, &amp;quot;readmitted&amp;quot;)) %&amp;gt;% 
  ungroup() %&amp;gt;% subset(gender==&amp;quot;Male&amp;quot;|gender==&amp;quot;Female&amp;quot;) %&amp;gt;%
  ggplot() +
  geom_col(aes(x = ifelse(readmission == &amp;quot;readmitted&amp;quot;, -n, n),
               y = age,
               fill = readmission)) +
  facet_wrap(~ gender) +
  scale_x_continuous(
    labels = function(n) scales::comma(abs(n))) +
  xlab(&amp;quot;Counts&amp;quot;)
p21&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-2-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;p22 &amp;lt;- dem %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% group_by(gender, age, readmitted) %&amp;gt;% summarise(n=n()) %&amp;gt;% mutate(freq = n / sum(n)) %&amp;gt;% subset(gender==&amp;quot;Male&amp;quot;|gender==&amp;quot;Female&amp;quot;)
ggplot(data=p22, aes(x=age, y=freq, fill=readmitted)) + geom_col() + facet_wrap(~ gender) + labs(y=&amp;quot;proportions&amp;quot;) + geom_text(aes(label = paste0(round(freq, 4) * 100, &amp;quot;%&amp;quot;)), position = position_stack(vjust = 0.5), size=2.5, angle=90) + theme(axis.text.x = element_text(angle=90, vjust=.5, hjust=1))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-2-2.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The population pyramid is an intriguing plot type, and already tells us that for most age groups, more women are readmitted. But this could be solely because there are more women than men in the sample. However, from the proportion barchart, we see here that proportion of readmitted women is greater than men, particularly for the 20-30 age group.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;patient-diagnoses&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.2.3 Patient diagnoses&lt;/h4&gt;
&lt;p&gt;Finally, how are readmission rates distributed by patient and patient care features. For example, how is it distributed by patient primary diagnosis? In the final section of this post, we will leverage &lt;code&gt;ggplot2&lt;/code&gt;’s visualization power to triangulate patient diagnoses with the key covariate and the response. Like in the previous section, we use proportions, adding the relevant labels to more easily infer that we see higher readmission rates for a diabetes diagnosis.&lt;/p&gt;
&lt;p&gt;We change around quite a few of the plotting parameters in &lt;code&gt;ggplot2&lt;/code&gt; to make it look much more eye-catching.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# How is readmitted rate distributed by diagnoses?
sqldf(&amp;#39;SELECT primary_diag, readmitted, COUNT(*) as n FROM results LEFT JOIN y USING(uid) GROUP BY primary_diag, readmitted&amp;#39;)  # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##    primary_diag readmitted     n
## 1   circulatory        &amp;lt;30  3485
## 2   circulatory        &amp;gt;30 10839
## 3   circulatory         NO 16113
## 4      diabetes        &amp;lt;30  1137
## 5      diabetes        &amp;gt;30  3318
## 6      diabetes         NO  4302
## 7         other        &amp;lt;30  5332
## 8         other        &amp;gt;30 15856
## 9         other         NO 26961
## 10  respiratory        &amp;lt;30  1403
## 11  respiratory        &amp;gt;30  5532
## 12  respiratory         NO  7488&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;p3 &amp;lt;- results %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% group_by(primary_diag, readmitted) %&amp;gt;% summarise(n=n()) %&amp;gt;% mutate(freq = n / sum(n))
ggplot(data=p3, aes(x=primary_diag, y=n, fill=readmitted)) + geom_bar(position = &amp;quot;dodge&amp;quot;, stat = &amp;quot;identity&amp;quot;, color=&amp;quot;black&amp;quot;) + scale_fill_manual(values=c(&amp;quot;#999999&amp;quot;, &amp;quot;#E69F00&amp;quot;, &amp;quot;#56B4E9&amp;quot;)) + theme_minimal() + labs(x=&amp;quot;Primary diagnoses&amp;quot;, y=&amp;quot;Counts (proportions as labels)&amp;quot;) + geom_text(aes(label = paste0(round(freq, 4) * 100, &amp;quot;%&amp;quot;)), position = position_dodge(width = 1), vjust=-0.7, size=3) + scale_y_continuous(labels = function(n) scales::comma(abs(n)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-3-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;hba1c-measurement&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;3.2.4 HbA1c measurement&lt;/h4&gt;
&lt;p&gt;One of the key questions this dataset seeks to answer is the &lt;em&gt;impact of the A1C test (decision to test) on readmission rates&lt;/em&gt;, in the presence of covariates (especially the primary diagnosis). Output in its raw form (i.e. untransformed) doesn’t always give us the answer clearly. To get around this, we will use &lt;code&gt;CASE WHEN&lt;/code&gt; in SQL and &lt;code&gt;mutate&lt;/code&gt; in &lt;code&gt;tidyverse&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let’s plot this in 2 ways – a barplot with labels, and a spineplot. The latter allows us to see the “weight” of the underlying categories.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# What is the readmission rate profile of patients who had their A1C measured?
sqldf(&amp;#39;SELECT CASE WHEN A1Cresult = &amp;quot;None&amp;quot; THEN &amp;quot;not measured&amp;quot; ELSE &amp;quot;measured&amp;quot; END AS a1c, readmitted,
   COUNT(*) FROM results LEFT JOIN y USING(uid) GROUP BY a1c, readmitted ORDER BY 1&amp;#39;) # SQL&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##            a1c readmitted COUNT(*)
## 1     measured        &amp;lt;30     1676
## 2     measured        &amp;gt;30     5800
## 3     measured         NO     9542
## 4 not measured        &amp;lt;30     9681
## 5 not measured        &amp;gt;30    29745
## 6 not measured         NO    45322&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;p4 &amp;lt;- results %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% mutate(a1c=ifelse(A1Cresult==&amp;quot;None&amp;quot;, &amp;quot;not measured&amp;quot;, &amp;quot;measured&amp;quot;)) %&amp;gt;% 
  group_by(a1c, readmitted) %&amp;gt;% summarise(n=n()) %&amp;gt;% mutate(freq = n / sum(n)) 
ggplot(data=p4, aes(x=a1c, y=freq, fill=readmitted)) + geom_col() + labs(x=&amp;quot;HbA1c test measurement&amp;quot;, y=&amp;quot;proportions&amp;quot;) + geom_text(aes(label = paste0(round(freq, 4) * 100, &amp;quot;%&amp;quot;)), position = position_stack(vjust = 0.5), size=3)# dplyr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-4-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# Spineplot
library(ggmosaic)
p5 &amp;lt;- results %&amp;gt;% left_join(y, by=join_by(uid)) %&amp;gt;% left_join(dem, by=join_by(uid))%&amp;gt;% mutate(a1c=ifelse(A1Cresult==&amp;quot;None&amp;quot;, &amp;quot;not measured&amp;quot;, &amp;quot;measured&amp;quot;)) %&amp;gt;% subset(gender==&amp;quot;Male&amp;quot;|gender==&amp;quot;Female&amp;quot;)
per &amp;lt;- p5 %&amp;gt;% group_by(a1c, readmitted) %&amp;gt;% summarise(n=n()) %&amp;gt;% mutate(freq = n / sum(n)) 
g &amp;lt;- ggplot(p5) + geom_mosaic(aes(x = product(a1c),fill = readmitted)) 

g + geom_text(data = ggplot_build(g)$data[[1]] %&amp;gt;% 
                group_by(x__a1c) %&amp;gt;%
                mutate(pct = .wt/sum(.wt)*100), 
              aes(x = (xmin+xmax)/2, y = (ymin+ymax)/2, label=paste0(round(pct, 2), &amp;quot;%&amp;quot;)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/index_files/figure-html/explore-4-2.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;p&gt;We observe a lower readmission rate (&amp;lt;30 days) when there is an A1C measurement taken, vs when it is not measured at all. In the 2nd/spineplot, we see this without actually calculating the percentages, while also inferring that number of patients not measured is much higher than those measured. We do however, manually add in the percentages to the spineplot to get a more complete picture on the relationship between HbA1c measurement and readmission rates.
These are key findings which we will explore in greater detail, using &lt;code&gt;tidyverse&lt;/code&gt; and &lt;code&gt;ggplot2&lt;/code&gt; more extensively, in the next part of this blog series, including cutting these plots across multiple covariates to explore how HbA1c affects readmissions in the presence of other patient groupings. Stay tuned!&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2023/04/06/a-data-analyst-workflow-part-1-sql-tidyverse/&#39;;&lt;/script&gt;
      </description>
    </item>
    
    <item>
      <title>In-Database Logistic Regression with R</title>
      <link>https://rviews.rstudio.com/2019/12/04/in-database-logisitc-regression-with-r/</link>
      <pubDate>Wed, 04 Dec 2019 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2019/12/04/in-database-logisitc-regression-with-r/</guid>
      <description>
        


&lt;p&gt;&lt;em&gt;Roland Stevenson is a data scientist and consultant who may be reached on &lt;a href=&#34;https://www.linkedin.com/in/roland-stevenson/&#34;&gt;Linkedin&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In a &lt;a href=&#34;https://rviews.rstudio.com/2018/11/07/in-database-xgboost-predictions-with-r/&#34;&gt;previous article&lt;/a&gt; we illustrated how to calculate xgboost model predictions in-database. This was &lt;a href=&#34;https://github.com/tidymodels/tidypredict/issues/40&#34;&gt;referenced&lt;/a&gt; and incorporated into &lt;a href=&#34;https://github.com/tidymodels/tidypredict&#34;&gt;tidypredict&lt;/a&gt;. After learning more about what the tidypredict team is up to, I discovered another tidyverse package called &lt;a href=&#34;https://github.com/tidymodels/modeldb&#34;&gt;modeldb&lt;/a&gt; that fits models in-database. It currently supports linear regression and k-means clustering, so I thought I would provide an example of how to do in-database logistic regression.&lt;/p&gt;
&lt;p&gt;Rather than focusing on the details of logistic regression, we will focus more on how we can use R and some carefully written SQL statements to iteratively minimize a cost function. We will also use the &lt;code&gt;condusco&lt;/code&gt; R package, which allows us to iterate through the results of a query easily.&lt;/p&gt;
&lt;div id=&#34;a-simple-logistic-regression-example&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;A Simple Logistic Regression Example&lt;/h2&gt;
&lt;p&gt;Let’s start with a simple logistic regression example. We’ll simulate an outcome &lt;span class=&#34;math inline&#34;&gt;\(y\)&lt;/span&gt; based on the fact that &lt;span class=&#34;math inline&#34;&gt;\(Pr(y=1) = \frac{e^{\beta x}}{1+e^{\beta x}}\)&lt;/span&gt;. Here &lt;span class=&#34;math inline&#34;&gt;\(\beta\)&lt;/span&gt; is a vector containing the coefficients we will later be estimating (including an intercept term). In the example below, our &lt;span class=&#34;math inline&#34;&gt;\(x\)&lt;/span&gt; values are uniform random values between -1 and 1.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;set.seed(1)

# the number of samples
n &amp;lt;- 1000

# uniform random on (-1,1)
x1 &amp;lt;- 2*runif(n)-1
x2 &amp;lt;- 2*runif(n)-1
x &amp;lt;- cbind(1, x1, x2)

# our betas
beta &amp;lt;- c(-1, -3.0, 5.0)

probs &amp;lt;- exp(beta %*% t(x))/(1+exp(beta %*% t(x)))

y &amp;lt;- rbinom(n,1,probs)

sim &amp;lt;- data.frame(id = seq(1:n), y = y, x1 = x1, x2 = x2)

mylogit &amp;lt;- glm(y ~ x1 + x2, data = sim, family = &amp;quot;binomial&amp;quot;)
summary(mylogit)
mylogit$coefficients&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;SQLA.png&#34; height = &#34;300&#34; width=&#34;100%&#34;&gt;&lt;/p&gt;
&lt;p&gt;As expected, the coefficients of our logistic model successfully approximate the parameters in our &lt;code&gt;beta&lt;/code&gt; vector.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;in-database-logistic-regression&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;In-database Logistic Regression&lt;/h2&gt;
&lt;p&gt;Now, let’s see if we can find a way to calculate these same coefficients in-database. In this example, we’re going to use Google BigQuery as our database, and we’ll use &lt;code&gt;condusco&lt;/code&gt;’s &lt;code&gt;run_pipeline_gbq&lt;/code&gt; function to iteratively run the functions we define later on. To do this, we’ll need to take care of some initial housekeeping:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(bigrquery)
library(whisker)
library(condusco)

# Uncomment and define your own config
# config &amp;lt;- list(
#   project = &amp;#39;&amp;lt;YOUR GBQ PROJECT&amp;gt;&amp;#39;,
#   dataset = &amp;#39;&amp;lt;YOUR GBQ DATASET&amp;gt;&amp;#39;,
#   table_prefix = &amp;#39;&amp;lt;A TABLE_PREFIX TO USE&amp;gt;&amp;#39;
# )

# a simple whisker.render helper function for our use-case
wr &amp;lt;- function(s, params=config){whisker.render(s,params)}

# put the simulated data in GBQ
insert_upload_job(
  project = wr(&amp;#39;{{{project}}}&amp;#39;),
  dataset = wr(&amp;#39;{{{dataset}}}&amp;#39;),
  table = &amp;quot;logreg_sim&amp;quot;,
  values = sim,
  write_disposition = &amp;quot;WRITE_TRUNCATE&amp;quot;
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;SQL1.png&#34; height = &#34;75&#34; width=&#34;100%&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now, we’ll create the pipelines to do the logistic regression. Please note that the code below is quite verbose. While all of it is needed for the code to work, we’ll just focus on understanding how a couple of steps work. Once we understand one step, the rest is pretty easy. Feel free to skip &lt;a href=&#34;#run-pipeline&#34;&gt;ahead&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First, we create a pipeline that does two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create a main table containing all of our global settings&lt;/li&gt;
&lt;li&gt;calls another pipeline (&lt;code&gt;log_reg_stack&lt;/code&gt;) with the global settings as inputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Importantly, note that all of the parameters (eg. &lt;code&gt;{{{project}}}&lt;/code&gt;) are dynamically swapped out in the query below with the &lt;code&gt;wr&lt;/code&gt; function and the &lt;code&gt;params&lt;/code&gt; variables. So this pipeline dynamically creates a query based on the parameters passed to it. We will call this pipeline later to run the process.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#
# Pipeline: log_reg
#
log_reg &amp;lt;- function(params){
  
  print (&amp;quot;log_reg&amp;quot;)

  query &amp;lt;- &amp;#39;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_settings
    AS 
    SELECT
      &amp;quot;{{{project}}}&amp;quot; AS project,
      &amp;quot;{{{dataset}}}&amp;quot; AS dataset,
      &amp;quot;{{{data_table}}}&amp;quot; AS data_table,
      {{{max_steps}}} AS max_steps,
      {{{error_tol}}} AS error_tol,
      {{{learning_rate}}} AS learning_rate,
      &amp;quot;{{{id_column}}}&amp;quot;   AS id_column,
      &amp;quot;{{{label_column}}}&amp;quot; AS label_column,
      &amp;quot;{{{fieldnames}}}&amp;quot; AS fieldnames,
      &amp;quot;{{{constant_id}}}&amp;quot; AS constant_id,
      &amp;quot;{{{table_prefix}}}&amp;quot; AS table_prefix
  &amp;#39;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  # Now run the log_reg_stack pipeline and pass the settings to it
  invocation_query &amp;lt;- &amp;#39;
    SELECT *
    FROM {{{dataset}}}.{{table_prefix}}_settings
  &amp;#39;
  run_pipeline_gbq(
    log_reg_stack,
    wr(invocation_query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above pipeline calls another pipeline, &lt;code&gt;log_reg_stack&lt;/code&gt;, which is defined below. &lt;code&gt;log_reg_stack&lt;/code&gt; creates a table with the field names that we will use in the logistic regression and then runs &lt;code&gt;log_reg_stack_field&lt;/code&gt; on each of the field names. Note that the &lt;code&gt;invocation_query&lt;/code&gt; below contains a query that results in one or more rows containing a field name. &lt;code&gt;run_pipeline_gbq&lt;/code&gt; takes the results and iterates over them, calling &lt;code&gt;log_reg_stack_field&lt;/code&gt; on each one. Finally, it creates the &lt;code&gt;_labels&lt;/code&gt; table and calls &lt;code&gt;log_reg_setup&lt;/code&gt;, passing it the results of the global settings query.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#
# Pipeline: stack variables
#
log_reg_stack &amp;lt;- function(params){
  
  print (&amp;quot;log_reg_stack&amp;quot;)
  
  # Table: _fieldnames 
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_fieldnames
    AS
    SELECT TRIM(fieldname) AS fieldname
    FROM (
      SELECT split(fieldnames,&amp;#39;,&amp;#39;) AS fieldname
      FROM (
          SELECT &amp;#39;{{{fieldnames}}}&amp;#39; AS fieldnames
      )
    ), UNNEST(fieldname) as fieldname
    GROUP BY 1
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  # Run _stack_field
  query &amp;lt;- &amp;quot;
    DROP TABLE IF EXISTS {{{dataset}}}.{{{table_prefix}}}_stacked
  &amp;quot;
  
  tryCatch({
    query_exec(
      project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
      query = wr(query, params),
      use_legacy_sql = FALSE
    )},
    error = function(e){
      print(e)
  })
    
  invocation_query &amp;lt;- &amp;quot;
    SELECT
      a.fieldname AS fieldname,  
      b.*
    FROM (  
      SELECT fieldname  
      FROM {{{dataset}}}.{{{table_prefix}}}_fieldnames  
      GROUP BY fieldname
    ) a  
    CROSS JOIN (  
      SELECT *  
        FROM {{{dataset}}}.{{{table_prefix}}}_settings
    ) b
  &amp;quot;
  run_pipeline_gbq(
    log_reg_stack_field,
    wr(invocation_query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
  # Table: _labels
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_labels
    AS
    SELECT
      {{{id_column}}} AS id,
      {{{label_column}}} AS label
    FROM {{{data_table}}}
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  
  # Run _setup
  invocation_query &amp;lt;- &amp;quot;
    SELECT *  
      FROM {{{dataset}}}.{{{table_prefix}}}_settings
  &amp;quot;
  run_pipeline_gbq(
    log_reg_setup,
    wr(invocation_query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;log_reg_stack_field&lt;/code&gt; and &lt;code&gt;log_reg_setup&lt;/code&gt; pipelines are not particularly interesting. They do the groundwork needed to allow the &lt;code&gt;log_reg_loop&lt;/code&gt; pipeline to iterate. The &lt;code&gt;_stacked&lt;/code&gt; table contains the feature names and their values, and the &lt;code&gt;_feature_stats&lt;/code&gt; and &lt;code&gt;features_stacked_vni&lt;/code&gt; tables contains normalized values used later. Finally, the &lt;code&gt;_fit_params&lt;/code&gt; table contains the value of the fit parameters that will be updated as we iteratively minimize the cost function in the loop. The &lt;code&gt;log_reg_setup&lt;/code&gt; pipeline ends by calling &lt;code&gt;log_reg_loop&lt;/code&gt;, passing it the results of the global settings query.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;log_reg_stack_field &amp;lt;- function(params){
  
  print (&amp;quot;log_reg_stack_field&amp;quot;)

  destination_table &amp;lt;- &amp;#39;{{{dataset}}}.{{{table_prefix}}}_stacked&amp;#39;

  query &amp;lt;- &amp;quot;
    SELECT {{{id_column}}} AS id,
      LTRIM(&amp;#39;{{{fieldname}}}&amp;#39;) AS feature_name,
      CAST({{{fieldname}}} AS FLOAT64) AS vi
    FROM {{{data_table}}}
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    destination_table = wr(destination_table, params),
    use_legacy_sql = FALSE,
    write_disposition = &amp;#39;WRITE_APPEND&amp;#39;,
    create_disposition = &amp;#39;CREATE_IF_NEEDED&amp;#39;
  )
  
}


log_reg_setup &amp;lt;- function(params){
  
  print (&amp;quot;log_reg_setup&amp;quot;)
  
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_feature_stats
    AS
    SELECT feature_name,
      AVG(vi) AS mean,
      STDDEV(vi) AS stddev
    FROM {{{dataset}}}.{{{table_prefix}}}_stacked
    GROUP BY feature_name
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_features_stacked_vni
    AS
    SELECT
      a.id AS id,
      a.feature_name AS feature_name,
      CASE
        WHEN b.stddev &amp;gt; 0.0 THEN (vi - b.mean) / b.stddev
        ELSE vi - b.mean
      END AS vni
    FROM {{{dataset}}}.{{{table_prefix}}}_stacked a
    JOIN {{{dataset}}}.{{{table_prefix}}}_feature_stats b
      ON a.feature_name = b.feature_name
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  query &amp;lt;- &amp;quot;
    INSERT INTO {{{dataset}}}.{{{table_prefix}}}_features_stacked_vni (id, feature_name, vni)     
    SELECT
      id,
      &amp;#39;{{{constant_id}}}&amp;#39; as feature_name,
      1.0 as vni
    FROM {{{dataset}}}.{{{table_prefix}}}_stacked
    GROUP BY 1,2,3
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_fit_params
    AS
    SELECT
      step,
      param_id,
      param_value,
      cost,
      stop,
      message
    FROM (
      SELECT 1 as step,
      feature_name as param_id,
      0.0 as param_value,
      1e6 as cost,
      false as stop,
      &amp;#39;&amp;#39; as message
      FROM {{{dataset}}}.{{{table_prefix}}}_stacked
      GROUP BY param_id
    ) UNION ALL (
      SELECT 1 as step,
      &amp;#39;{{{constant_id}}}&amp;#39; as param_id,
      0.0 as param_value,
      1e6 as cost,
      false as stop,
      &amp;#39;&amp;#39; as message
    )
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  # Run _loop
  invocation_query &amp;lt;- &amp;quot;
    SELECT *  
      FROM {{{dataset}}}.{{{table_prefix}}}_settings
  &amp;quot;
  run_pipeline_gbq(
    log_reg_loop,
    wr(invocation_query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, we’ll create a loop pipeline that will iteratively calculate the cost function and update the &lt;code&gt;_fit_params&lt;/code&gt; table with the latest update.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#
# Pipeline: loop
#
log_reg_loop &amp;lt;- function(params){
  
  print (&amp;quot;log_reg_loop&amp;quot;)
  
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_x_dot_beta_i
    AS 
    SELECT
      a.id AS id,
      SUM(a.vni * b.param_value) AS x_dot_beta_i
    FROM {{{dataset}}}.{{{table_prefix}}}_features_stacked_vni a  
    RIGHT JOIN (
      SELECT param_id, param_value    
      FROM {{{dataset}}}.{{{table_prefix}}}_fit_params    
      WHERE STEP = (SELECT max(step) FROM {{{dataset}}}.{{{table_prefix}}}_fit_params)
    ) b
    ON a.feature_name = b.param_id
    GROUP BY 1
  &amp;quot;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  query &amp;lt;- &amp;#39;
  INSERT INTO {{{dataset}}}.{{{table_prefix}}}_fit_params (step, param_id, param_value, cost, stop, message)
  SELECT  
    b.step + 1 as step,  
    b.param_id as param_id,  
    b.param_value - {{{learning_rate}}} * err as param_value,
    -1.0 * a.cost as cost,
    CASE
      WHEN ( abs((b.cost-(-1.0*a.cost))/b.cost) &amp;lt; {{{error_tol}}} ) OR (step+1 &amp;gt; {{{max_steps}}})  
        THEN true
      ELSE false  
      END AS stop,  
    CONCAT( &amp;quot;cost: &amp;quot;, CAST(abs((b.cost-(-1.0*a.cost))/b.cost) AS STRING), &amp;quot; error_tol: &amp;quot;, CAST({{{error_tol}}} AS STRING)) as message  
  FROM (  
    SELECT  
      param_id,  
      avg(err) as err,  
      avg(cost) as cost  
    FROM (  
      SELECT  
        a.id,
        param_id,
        (1.0/(1.0 + EXP(-1.0 * (c.x_dot_beta_i))) - CAST(label AS FLOAT64)) * vni as err,
        CAST(label AS FLOAT64) * LOG( 1.0/(1.0 + EXP(-1.0 * (c.x_dot_beta_i))) )   
          + (1.0-CAST(label AS FLOAT64))*(log(1.0 - (1.0/(1.0 + EXP(-1.0 * (c.x_dot_beta_i))))))  as cost
      FROM (  
        SELECT a.id as id,  
        b.param_id as param_id,
        a.vni as vni,
        b.param_value as param_value
        FROM {{{dataset}}}.{{{table_prefix}}}_features_stacked_vni a  
        JOIN (
          SELECT param_id, param_value    
          FROM {{{dataset}}}.{{{table_prefix}}}_fit_params    
          WHERE STEP = (SELECT max(step) FROM {{{dataset}}}.{{{table_prefix}}}_fit_params)
        ) b
        ON a.feature_name = b.param_id
        GROUP BY 1,2,3,4
      ) a
      JOIN {{{dataset}}}.{{{table_prefix}}}_labels b  
      ON a.id = b.id
      JOIN {{{dataset}}}.{{{table_prefix}}}_x_dot_beta_i c
      ON a.id = c.id
    )  
    GROUP BY param_id
  ) a
  JOIN (
    SELECT *
    FROM {{{dataset}}}.{{{table_prefix}}}_fit_params
    WHERE STEP = (SELECT max(step) FROM {{{dataset}}}.{{{table_prefix}}}_fit_params)
  ) b
  ON a.param_id = b.param_id
  &amp;#39;
  
  query_exec(
    project = wr(&amp;#39;{{{project}}}&amp;#39;, params),
    query = wr(query, params),
    use_legacy_sql = FALSE
  )
  
  
  # Loop or stop
  query &amp;lt;- &amp;quot;
      SELECT stop  AS stop
      FROM (
        SELECT *
          FROM {{{dataset}}}.{{{table_prefix}}}_fit_params
        ORDER BY step DESC
        LIMIT 1
      )
  &amp;quot;
  
  res &amp;lt;- query_exec(
    wr(query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
  if(res$stop == FALSE){
    print(&amp;quot;stop == FALSE&amp;quot;)
    invocation_query &amp;lt;- &amp;#39;
      SELECT *
      FROM {{{dataset}}}.{{table_prefix}}_settings
    &amp;#39;
    run_pipeline_gbq(
      log_reg_loop,
      wr(invocation_query,  params),
      wr(&amp;#39;{{{project}}}&amp;#39;, params),
      use_legacy_sql = FALSE
    )
  }
  else {
    print(&amp;quot;stop == TRUE&amp;quot;)
    invocation_query &amp;lt;- &amp;#39;
      SELECT *
      FROM {{{dataset}}}.{{table_prefix}}_settings
    &amp;#39;
    run_pipeline_gbq(
      log_reg_done,
      wr(invocation_query,  params),
      wr(&amp;#39;{{{project}}}&amp;#39;, params),
      use_legacy_sql = FALSE
    )
  }
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And finally, a &lt;code&gt;log_reg_done&lt;/code&gt; pipeline that outputs the results:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#
# Pipeline: done
#
log_reg_done &amp;lt;- function(params){
  
  print (&amp;quot;log_reg_done&amp;quot;)
  
  # Display results in norm&amp;#39;d coords
  query &amp;lt;- &amp;#39;
    SELECT &amp;quot;normalized coords parameters&amp;quot; as message,
      step,  
      param_id,  
      param_value 
    FROM {{{dataset}}}.{{{table_prefix}}}_fit_params
    WHERE step = (SELECT max(step) from {{{dataset}}}.{{{table_prefix}}}_fit_params)
  &amp;#39;
  
  res &amp;lt;- query_exec(
    wr(query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
  print(res)
  
  # Display results in original coords
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_model_params_stacked
    AS 
    SELECT
      param_id,
      param_value_rescaled
    FROM (
      SELECT
        a.param_id AS param_id,
        a.param_value + b.constant_offset AS param_value_rescaled
      FROM (
        SELECT
          step,
          param_id,
          param_value
        FROM {{{dataset}}}.{{{table_prefix}}}_fit_params
        WHERE step = (SELECT max(step) from {{{dataset}}}.{{{table_prefix}}}_fit_params)
        AND param_id = &amp;#39;CONSTANT&amp;#39;
      ) a
      JOIN (
        SELECT
          step,
          &amp;#39;CONSTANT&amp;#39; as param_id,
          sum(-1.0*param_value*mean/stddev) as constant_offset
        FROM {{{dataset}}}.{{{table_prefix}}}_fit_params a
        JOIN {{{dataset}}}.{{{table_prefix}}}_feature_stats b
          ON a.param_id = b.feature_name
        WHERE step = (SELECT max(step) FROM {{{dataset}}}.{{{table_prefix}}}_fit_params)
        GROUP BY 1,2
      ) b
      ON a.param_id = b.param_id
    ) UNION ALL (
      SELECT
        param_id,
        param_value/stddev as param_value_rescaled
      FROM {{{dataset}}}.{{{table_prefix}}}_fit_params a
      JOIN {{{dataset}}}.{{{table_prefix}}}_feature_stats b
      ON a.param_id = b.feature_name
      WHERE step = (SELECT max(step) FROM {{{dataset}}}.{{{table_prefix}}}_fit_params)
      GROUP BY 1,2
    )
  &amp;quot;
  
  res &amp;lt;- query_exec(
    wr(query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
  print(res)
  
  
  # transpose the _model_params_stacked table
  invocation_query &amp;lt;- &amp;#39;
    SELECT
      a.list,
      b.*
    FROM (
      SELECT CONCAT(&amp;quot;[&amp;quot;, STRING_AGG(CONCAT(&amp;quot;{\\&amp;quot;val\\&amp;quot;: \\&amp;quot;&amp;quot;,TRIM(fieldname), &amp;quot;\\&amp;quot;}&amp;quot;)), &amp;quot;]&amp;quot;) AS list
      FROM rstevenson.indb_logreg_001_fieldnames
    ) a
    CROSS JOIN (
      SELECT *
        FROM rstevenson.indb_logreg_001_settings
    ) b
  &amp;#39;
  
  run_pipeline_gbq(
    log_reg_model_params,
    wr(invocation_query, config),
    wr(&amp;#39;{{{project}}}&amp;#39;, config),
    use_legacy_sql = FALSE
  )
  
  print(&amp;quot;DONE&amp;quot;)
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our last pipeline, called at the end of the above pipeline, will transpose the stacked model params. In other words, it will output the parameters of the model in separate columns:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;log_reg_model_params &amp;lt;- function(params){
  
  query &amp;lt;- &amp;quot;
    CREATE OR REPLACE TABLE {{{dataset}}}.{{{table_prefix}}}_model_params
    AS 
    SELECT
    {{#list}}
      MAX(CASE WHEN param_id=&amp;#39;{{val}}&amp;#39; THEN param_value_rescaled END ) AS {{val}},
    {{/list}}
    MAX(CASE WHEN param_id=&amp;#39;{{constant_id}}&amp;#39; THEN param_value_rescaled END ) AS {{constant_id}}
    FROM {{{dataset}}}.{{{table_prefix}}}_model_params_stacked
  ;&amp;quot;

  res &amp;lt;- query_exec(
    wr(query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
  print(res)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;run-pipeline&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Running the pipeline&lt;/h2&gt;
&lt;p&gt;We are now ready to run the &lt;code&gt;log_reg&lt;/code&gt; pipeline. We’ll set up the invocation query with all of our global parameters. These will be stored in the &lt;code&gt;_settings&lt;/code&gt; table and then, after stacking and setup, the pipeline will iterate through the loop to calculate the logistic regression coefficients.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# Run the log_reg pipeline with the following params (2D test)
invocation_query &amp;lt;- &amp;#39;
  SELECT
  &amp;quot;{{{project}}}&amp;quot; as project,
  &amp;quot;{{{dataset}}}&amp;quot; as dataset,
  &amp;quot;{{{table_prefix}}}&amp;quot; as table_prefix,
  &amp;quot;{{{dataset}}}.logreg_sim&amp;quot; as data_table,        
  &amp;quot;25&amp;quot;  as max_steps,
  &amp;quot;1e-6&amp;quot; as error_tol,
  &amp;quot;6.0&amp;quot;  as learning_rate,
  &amp;quot;id&amp;quot;   as id_column,
  &amp;quot;y&amp;quot;  as label_column,
  &amp;quot;x1, x2&amp;quot;  as fieldnames,
  &amp;quot;CONSTANT&amp;quot; as constant_id
&amp;#39;

cat(wr(invocation_query, config))

query_exec(wr(invocation_query, config), project=config$project, use_legacy_sql = FALSE)

run_pipeline_gbq(
  log_reg,
  wr(invocation_query, config),
  project = wr(&amp;#39;{{{project}}}&amp;#39;, config),
  use_legacy_sql = FALSE
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After running the above, we should be able to query the table that holds the fitted parameters:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;  query &amp;lt;- &amp;quot;
    SELECT *
    FROM {{{dataset}}}.{{{table_prefix}}}_model_params
  ;&amp;quot;

  query_exec(
    wr(query),
    wr(&amp;#39;{{{project}}}&amp;#39;),
    use_legacy_sql = FALSE
  )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;SQL2.png&#34; height = &#34;75&#34; width=&#34;100%&#34;&gt;&lt;/p&gt;
&lt;p&gt;As expected, these results are pretty close to our original &lt;code&gt;beta&lt;/code&gt; values.&lt;/p&gt;
&lt;p&gt;Please keep in mind that this is not ready to be released into the wild. Further improvements include modifications to deal with categorical variables, output describing whether a logistic fit is statistically significant for a particular parameter, and options for controlling step-sizes. But it does show the concept of how an iterative process like logistic regression can be done while using the database to maintain state.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;prediction&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Prediction&lt;/h2&gt;
&lt;p&gt;Now that we have fit the logistic regression model and the model is stored in the database, we can predict values using the model. We just need a prediction pipeline:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#
# Pipeline: predict
#
log_reg_predict &amp;lt;- function(params){
  
  query &amp;lt;- &amp;#39;
  SELECT
    1/(1+exp(-1.0*(CONSTANT + {{#list}}a.{{val}}*b.{{val}} + {{/list}} + 0))) as probability
  FROM {{{dataset}}}.{{{table_prefix}}}_model_params a
  CROSS JOIN {{{data_table}}} b
  ORDER BY {{{id_column}}}
  &amp;#39;
  
  res &amp;lt;- query_exec(
    wr(query, params),
    wr(&amp;#39;{{{project}}}&amp;#39;, params),
    use_legacy_sql = FALSE
  )
  
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the above uses &lt;code&gt;whisker&lt;/code&gt; to calculate the dot product &lt;span class=&#34;math inline&#34;&gt;\(x\beta\)&lt;/span&gt; by expanding a JSON-formatted array of field names into &lt;code&gt;{{#list}}a.{{val}}*b.{{val}} + {{/list}}&lt;/code&gt; code. In the code below, we will create a JSON-formatted array of field names. Now let’s run the predictions:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# Run the prediction pipeline with the following params
invocation_query &amp;lt;- &amp;#39;
  SELECT
    &amp;quot;{{{project}}}&amp;quot; as project,
    &amp;quot;{{{dataset}}}&amp;quot; as dataset,
    &amp;quot;{{{table_prefix}}}&amp;quot; as table_prefix,
    &amp;quot;{{{dataset}}}.logreg_sim&amp;quot; as data_table,
    &amp;quot;id&amp;quot; as id_column,
    CONCAT(&amp;quot;[&amp;quot;, STRING_AGG(CONCAT(&amp;quot;{\\&amp;quot;val\\&amp;quot;: \\&amp;quot;&amp;quot;,TRIM(fieldname), &amp;quot;\\&amp;quot;}&amp;quot;)), &amp;quot;]&amp;quot;) AS list
  FROM {{{dataset}}}.{{{table_prefix}}}_fieldnames
&amp;#39;

predictions &amp;lt;- run_pipeline_gbq(
  log_reg_predict,
  wr(invocation_query, config),
  project = wr(&amp;#39;{{{project}}}&amp;#39;, config),
  use_legacy_sql = FALSE
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s test the rounded predictions to see how well they approximate the outcomes:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# inspect first 5 true probs vs. predicted probabilities
head(probs[1:5])
head(predictions[[1]]$probability[1:5])

# mean relative error between true probs and predicted probabilities 
mean((abs(probs-predictions[[1]]$probability))/probs)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;SQL3.png&#34; height = &#34;200&#34; width=&#34;100%&#34;&gt;&lt;/p&gt;
&lt;p&gt;Our model-based logistic regression model predicts the true probabilities with a mean relative error of about 7%.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;next-steps&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Next steps&lt;/h2&gt;
&lt;p&gt;We have shown how to train and store a logistic regression model in a database. We can then predict outcomes given features that are also stored in the database without having to move data back and forth to a prediction server. In this particular example, it would likely be much faster to move the data to a computer and run the predictions there. However, certain use cases exist where in-database modeling could be an avenue for consideration. Further, since logistic models are fundamental to many types of tree and forest predictors, in-database logistic regression would be a necessary step in developing in-database tree methods. It remains to be seen if this approach can be easily translated into the tidyverse modeldb package.&lt;/p&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2019/12/04/in-database-logisitc-regression-with-r/&#39;;&lt;/script&gt;
      </description>
    </item>
    
    <item>
      <title>In-database xgboost predictions with R</title>
      <link>https://rviews.rstudio.com/2018/11/07/in-database-xgboost-predictions-with-r/</link>
      <pubDate>Wed, 07 Nov 2018 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2018/11/07/in-database-xgboost-predictions-with-r/</guid>
      <description>
        &lt;p&gt;Moving predictive machine learning algorithms into large-scale production environments can present many challenges.  For example, problems arise when attempting to calculate prediction probabilities (&amp;ldquo;scores&amp;rdquo;) for many thousands of subjects using many thousands of features located on remote databases.&lt;/p&gt;

&lt;p&gt;&lt;a href=&#34;https://cran.r-project.org/web/packages/xgboost/&#34;&gt;&lt;code&gt;xgboost&lt;/code&gt;&lt;/a&gt; (&lt;a href=&#34;https://xgboost.readthedocs.io/en/latest/index.html&#34;&gt;docs&lt;/a&gt;), a popular algorithm for classification and regression, and the model of choice in many winning &lt;a href=&#34;https://www.kaggle.com/&#34;&gt;Kaggle&lt;/a&gt; competitions, is no exception. However, to run &lt;code&gt;xgboost&lt;/code&gt;, the subject-features matrix &lt;a href=&#34;https://xgboost.readthedocs.io/en/latest/faq.html#i-have-a-big-dataset&#34;&gt;must be loaded into memory&lt;/a&gt;, a &lt;strong&gt;cumbersome and expensive&lt;/strong&gt; process.&lt;/p&gt;

&lt;p&gt;Available solutions require using expensive high-memory machines, or implementing external memory across distributed machines (expensive and in &lt;a href=&#34;https://xgboost.readthedocs.io/en/latest/tutorials/external_memory.html&#34;&gt;beta&lt;/a&gt;). Both solutions still require transferring all feature data from the database to the local machine(s), loading it into memory, calculating the probabilities for the subjects, and then transferring the probabilities back to the database for storage. I have seen this take, &lt;strong&gt;20-50 minutes for ~1MM subjects&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this post, we will consider &lt;strong&gt;in-database scoring&lt;/strong&gt;, a simple alternative for calculating batch predictions without having to transfer features stored in a database to the machine where the model is located. Instead, we will convert the model predictions into SQL commands and thereby transfer the scoring process to the database.&lt;/p&gt;

&lt;p&gt;&lt;img src=&#34;/post/2018-11-05-Roland-xgboost_files/xgboost_workflows.PNG&#34; height = &#34;400&#34; width=&#34;600&#34;&gt;&lt;/p&gt;

&lt;p&gt;We will convert the &lt;code&gt;xgboost&lt;/code&gt; model prediction process into a SQL query, and thereby accomplish the same task while leveraging a cloud database&amp;rsquo;s scalability to efficiently calculate the predictions.&lt;/p&gt;

&lt;p&gt;To accomplish this, we&amp;rsquo;ll need to work through a few steps.  First, we&amp;rsquo;ll import the model as a list of nested tree structures that we can iterate through recursively.  Then, we&amp;rsquo;ll create a function that will recursively descend through a tree and translate it into a SQL CASE statement. After that, we&amp;rsquo;ll create a query that sums the CASE statements for all trees before logit-transforming it to calculate a probability.&lt;/p&gt;

&lt;p&gt;This first block of code loads the required packages and converts the model object to a list of trees that we can work with:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-r&#34;&gt;library(xgboost)
library(jsonlite) 
library(whisker)

# our model exists in the variable `xgb_model`:
# dump the list of trees as JSON and import it as `model_trees` using jsonlite
model_trees &amp;lt;- jsonlite::fromJSON(
  xgb.dump(xgb_model, with_stats = FALSE, dump_format=&#39;json&#39;), 
  simplifyDataFrame = FALSE)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src=&#34;/post/2018-11-05-Roland-xgboost_files/xgboost_tree_structure.PNG&#34; height = &#34;400&#34; width=&#34;600&#34;&gt;&lt;/p&gt;

&lt;p&gt;Now, we need to translate each tree into a SQL CASE statement. Each tree represents a set of decisions based on whether a variable (the &amp;lsquo;split&amp;rsquo;) is less than a threshold value (the &amp;lsquo;split_condition&amp;rsquo;).  The result of the decision could be &amp;lsquo;yes&amp;rsquo;, &amp;lsquo;no&amp;rsquo;, or &amp;lsquo;missing&amp;rsquo;.  In each case, the tree provides the &amp;lsquo;node_id&amp;rsquo; of the next decision to evaluate.  When we reach a leaf, no decision needs to be made and instead a value is returned.  An example tree is shown below:&lt;/p&gt;

&lt;p&gt;We&amp;rsquo;ll also need a dictionary that maps an integer to its associated feature name, since the trees themselves refer to 0-indexed integers instead of the feature names.  We can accomplish that by creating the following list:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-r&#34;&gt;feature_dict &amp;lt;- as.list(xgb_model$feature_names)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Using our &lt;code&gt;feature_dict&lt;/code&gt; object, we can recursively descend through the tree and translate each node into a CASE statement, producing a sequence of nested CASE statements.  The following function does just that:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-r&#34;&gt;xgb_tree_sql &amp;lt;- function(tree, feature_dict, sig=5){
  # split variables must exist to generate subquery for tree children
  sv &amp;lt;- c(&amp;quot;split&amp;quot;, &amp;quot;split_condition&amp;quot;, &amp;quot;yes&amp;quot;, &amp;quot;no&amp;quot;, &amp;quot;missing&amp;quot;, &amp;quot;children&amp;quot;)

  # we have a leaf, just return the leaf value  
  if(&amp;quot;leaf&amp;quot; %in% names(tree)){
    return(round(tree[[&#39;leaf&#39;]],sig))
  }

  else if(all(sv %in% names(tree))){
    tree$split_long &amp;lt;- feature_dict[[tree$split+1]] # +1 because xgboost is 0-indexed
    
    cs &amp;lt;- c(tree$yes, tree$no, tree$missing)
    cd &amp;lt;- data.frame(
      k = c(min(cs), max(cs)),
      v = c(1,2)
    )

    tree$missing_sql &amp;lt;- xgb_tree_sql(tree$children[[cd$v[cd$k==tree$missing]]], feature_dict)
    tree$yes_sql &amp;lt;- xgb_tree_sql(tree$children[[cd$v[cd$k==tree$yes]]], feature_dict)
    tree$no_sql &amp;lt;- xgb_tree_sql(tree$children[[cd$v[cd$k==tree$no]]], feature_dict)
    
    q &amp;lt;- &amp;quot;
      CASE 
        WHEN {{{split_long}}} IS NULL THEN {{{missing_sql}}}
        WHEN {{{split_long}}} &amp;lt; {{{split_condition}}} THEN {{{yes_sql}}}
        ELSE {{{no_sql}}}
      END
    &amp;quot;

    return(whisker.render(q,tree))    
    
  }
  
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When we transform one tree into a sequence of nested CASE statements, we are producing a statement that yields that tree&amp;rsquo;s contribution to the total score.  We now need to sum the output of each tree and then calculate the total probability prediction.  In other words, we need to add up a list of nested CASE statements and then logit-transform the result.&lt;/p&gt;

&lt;p&gt;Note that below we make use of the R &lt;code&gt;whisker&lt;/code&gt; package.  This logic-less templating language is a great way to easily transform associative-arrays into SQL that contains easily identifiable labels as placeholders.  We find this more readable than sequences of &lt;code&gt;paste&lt;/code&gt; statements.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-r&#34;&gt;xgb_sql_score_query &amp;lt;- function(list_of_trees, features_table, feature_dict, key_field = &amp;quot;id&amp;quot;){
  
  # a swap list to render queries via whisker
  swap &amp;lt;- list(
    key_field = key_field,
    features_table = features_table
  )
    
  # score_queries contains the score query for each tree in the list_of_trees
  score_queries &amp;lt;- lapply(list_of_trees, function(tree){
    xgb_tree_sql(tree, feature_dict)
  })

  # the query clause to sum the scores from each tree 
  swap$sum_of_scores &amp;lt;- paste(score_queries, collapse=&#39; + &#39;)
        
  # score query that logit-transforms the sum_of_scores 
  q &amp;lt;- &amp;quot;
    SELECT
      {{{key_field}}},
      1/(1+exp(-1*( {{{sum_of_scores}}} ))) AS score
    FROM `{{{features_table}}}`
  &amp;quot;

  return(whisker.render(q,swap))

}

&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We are now ready to generate the score query from our model:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-r&#34;&gt;queries &amp;lt;- xgb_sql_score_query(
  model trees, 
  &#39;mydataset.my_feature_table&#39;,
  feature_dict
)

for(q in queries){
  # example: run the query with the R bigrquery package
  bq_project_query(&#39;my_project&#39;, q)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;In summary, production models typically calculate predictions for &lt;strong&gt;all subjects&lt;/strong&gt; on a daily, hourly, or even more frequent basis; however, moving feature data between a database and a local &amp;ldquo;scoring&amp;rdquo; machine is expensive and slow.  Transferring the scoring calculations to run within the database, as we&amp;rsquo;ve shown above, can significantly reduce both cost and run time.&lt;/p&gt;

&lt;p&gt;The astute reader may notice that, depending on the database, this will only work for a limited number of trees.  When that becomes a problem, it is possible to add another layer that stores the summed scores for batches of trees as views or tables, and then aggregates their results.  Beyond that, when queries with views become too long, it is possible to add an additional layer than aggregates batches of views into tables. We will save all of this for a future post.&lt;/p&gt;

&lt;p&gt;Roland Stevenson is a data scientist and consultant who may be reached on &lt;a href=&#34;https://www.linkedin.com/in/roland-stevenson/&#34;&gt;Linkedin&lt;/a&gt;&lt;/p&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2018/11/07/in-database-xgboost-predictions-with-r/&#39;;&lt;/script&gt;
      </description>
    </item>
    
  </channel>
</rss>
