<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>stevenmortimer.com</title>
    <link>https://stevenmortimer.com/blog/</link>
    <description>Recent content in Blog on stevenmortimer.com</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>Steven M. Mortimer</managingEditor>
    <webMaster>Steven M. Mortimer</webMaster>
    <copyright>This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.</copyright>
    <lastBuildDate>Sat, 12 Sep 2020 15:35:33 +0000</lastBuildDate>
    
        <atom:link href="https://stevenmortimer.com/blog/index.xml" rel="self" type="application/rss+xml" />
    
    
    <item>
      <title>salesforcer 0.2.2 - Relationship Queries and the Reports API</title>
      <link>https://stevenmortimer.com/salesforcer-0.2.2-relationship-queries-and-the-reports-api/</link>
      <pubDate>Sat, 12 Sep 2020 15:35:33 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/salesforcer-0.2.2-relationship-queries-and-the-reports-api/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/salesforcer-0.2.2---relationship-queries-and-the-reports-api/salesforcer-logo.png&#34; style=&#34;display:block;margin:auto;max-height:300px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The latest version of the {salesforcer} package (v0.2.2) is now available on CRAN
and is ready to help you have better access to data in your Salesforce Org. Along
with a host of bug fixes this release has three big features:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;&lt;p&gt;&lt;u&gt;&lt;strong&gt;Experimental Functions for the Reports and Dashboards REST API&lt;/strong&gt;&lt;/u&gt; (jump
to &lt;a href=&#34;#experimental-functions-for-reports-and-dashboards-rest-api&#34;&gt;section&lt;/a&gt;) - Now
you have programmatic access to executing and managing reports, dashboards, and
analytics notifications in your Org. You can familiarize yourself with the
Salesforce documentation &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;  href=&#34;https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_intro.htm&#34;&gt;HERE&lt;/a&gt;. Not all functions have been implemented yet, but your questions, comments,
and feedback are welcome!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;u&gt;&lt;strong&gt;Support for Bulk 2.0 API Queries&lt;/strong&gt;&lt;/u&gt; (jump to
&lt;a href=&#34;#support-for-bulk-2.0-api-queries&#34;&gt;section&lt;/a&gt;) - In Salesforce version 47.0
(Winter ’20) query jobs in the Bulk API 2.0 were added. Now you can leverage
this resource from the {salesforcer} package in addition to the queries via
the REST, SOAP, and Bulk 1.0 APIs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;u&gt;&lt;strong&gt;Support for Relationship Queries&lt;/strong&gt;&lt;/u&gt; (jump to
&lt;a href=&#34;#support-for-relationship-queries&#34;&gt;section&lt;/a&gt;) - In previous versions of the package the
child-to-parent and nested parent-to-child queries did not work or returned a
jumbled mess of results that were not parsed correctly. This version fixes those
issues in both the REST and SOAP APIs with better test coverage on a variety of
query types.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a complete list of updates, please review the release notes from v0.2.0 onwards
listed on the {salesforcer} pkgdown site here: &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/news/index.html&#34;&gt;https://stevenmmortimer.github.io/salesforcer/news/index.html&lt;/a&gt;.&lt;/p&gt;
&lt;div id=&#34;experimental-functions-for-reports-and-dashboards-rest-api&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Experimental Functions for Reports and Dashboards REST API&lt;/h2&gt;
&lt;p&gt;Salesforce has rich support for Reports in your Salesforce Org. Sometimes
reports are a better way to collaborate with other users because they can create
reports in the GUI or you can create one for them so they always have access to
the most current recordset meeting your report criteria. The challenge comes
when trying to access this data programmatically. Fortunately, Salesforce
provides the &lt;a target=&#34;_blank&#34;
href=&#34;https://developer.salesforce.com/docs/atlas.en-us.api_analytics.meta/api_analytics/sforce_analytics_rest_api_intro.htm&#34;&gt;Reports
and Dashboards REST API&lt;/a&gt; as a means to not only execute Reports, but to also
manage them in your Org.&lt;/p&gt;
&lt;p&gt;In Salesforce there is a dedicated page to displaying the list of reports in your
Org. It typically follows the pattern: &lt;code&gt;https://na1.salesforce.com/00O/o&lt;/code&gt;
(replace &lt;code&gt;na1&lt;/code&gt; with your server instance). When you click on a report in the GUI
you should see the report’s results. Below is a screenshot of how a report may
look in your Org (note the report Id in the URL bar):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/salesforcer-0.2.2---relationship-queries-and-the-reports-api/report-screenshot.png&#34; style=&#34;display:block;margin:auto;max-height:500px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The report Id above (&lt;code&gt;&#34;00O3s000006tE7zEAE&#34;&lt;/code&gt;) is the only information needed to pull
those same results from an R session, like so:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;my_report_id &amp;lt;- &amp;quot;00O3s000006tE7zEAE&amp;quot;
results &amp;lt;- sf_run_report(my_report_id)
results
#&amp;gt; # A tibble: 14 x 8
#&amp;gt;   `Contact ID` `First Name` `test number` `Contact Owner` `Account ID`
#&amp;gt;   &amp;lt;chr&amp;gt;        &amp;lt;chr&amp;gt;                &amp;lt;dbl&amp;gt; &amp;lt;chr&amp;gt;           &amp;lt;chr&amp;gt;       
#&amp;gt; 1 0036A000002… Edna                    NA Steven Mortimer 0016A000003…
#&amp;gt; 2 0036A000002… Liz                     NA Steven Mortimer 0016A000003…
#&amp;gt; 3 0036A000002… Tom                     NA Steven Mortimer 0016A000003…
#&amp;gt; 4 0036A000002… Sean                    NA Steven Mortimer 0016A000003…
#&amp;gt; 5 0036A000002… Rose                    NA Steven Mortimer 0016A000003…
#&amp;gt; # … with 9 more rows, and 3 more variables: `Account Name` &amp;lt;chr&amp;gt;, `Billing
#&amp;gt; #   City` &amp;lt;chr&amp;gt;, `Account Owner` &amp;lt;chr&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Currently, all of the report related functionality in the Reports and Dashboards
REST API has been ported into the {salesforcer} package and you can do some pretty
neat stuff like on-the-fly filtering and sorting:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# filter records that was created before this month
filter1 &amp;lt;- list(column = &amp;quot;CREATED_DATE&amp;quot;,
                        operator = &amp;quot;lessThan&amp;quot;, 
                        value = &amp;quot;THIS_MONTH&amp;quot;)

# filter records where the account billing address city is not empty
filter2 &amp;lt;-  list(column = &amp;quot;ACCOUNT.ADDRESS1_CITY&amp;quot;,
                        operator = &amp;quot;notEqual&amp;quot;, 
                        value = &amp;quot;&amp;quot;)

# combine filter1 and filter2 using &amp;#39;AND&amp;#39; which means that records must meet both filters
results_using_AND &amp;lt;- sf_run_report(my_report_id, 
                                   report_boolean_logic = &amp;quot;1 AND 2&amp;quot;,
                                   report_filters = list(filter1, filter2))
results_using_AND
#&amp;gt; # A tibble: 14 x 8
#&amp;gt;   `Contact ID` `First Name` `test number` `Contact Owner` `Account ID`
#&amp;gt;   &amp;lt;chr&amp;gt;        &amp;lt;chr&amp;gt;                &amp;lt;dbl&amp;gt; &amp;lt;chr&amp;gt;           &amp;lt;chr&amp;gt;       
#&amp;gt; 1 0036A000002… Rose                    NA Steven Mortimer 0016A000003…
#&amp;gt; 2 0036A000002… Sean                    NA Steven Mortimer 0016A000003…
#&amp;gt; 3 0036A000002… Jack                    99 Steven Mortimer 0016A000003…
#&amp;gt; 4 0036A000002… Tim                     NA Steven Mortimer 0016A000003…
#&amp;gt; 5 0036A000002… John                    23 Steven Mortimer 0016A000003…
#&amp;gt; # … with 9 more rows, and 3 more variables: `Account Name` &amp;lt;chr&amp;gt;, `Billing
#&amp;gt; #   City` &amp;lt;chr&amp;gt;, `Account Owner` &amp;lt;chr&amp;gt;

# combine filter1 and filter2 using &amp;#39;OR&amp;#39; which means that records must meet one 
# of the filters but also throw in a row limit based on a specific sort order
results_using_OR &amp;lt;- sf_run_report(my_report_id, 
                                  report_boolean_logic = &amp;quot;1 OR 2&amp;quot;,
                                  report_filters = list(filter1, filter2), 
                                  sort_by = &amp;quot;Contact.test_number__c&amp;quot;, 
                                  decreasing = TRUE, 
                                  top_n = 3)
results_using_OR
#&amp;gt; # A tibble: 3 x 8
#&amp;gt;   `Contact ID` `First Name` `test number` `Contact Owner` `Account ID`
#&amp;gt;   &amp;lt;chr&amp;gt;        &amp;lt;chr&amp;gt;                &amp;lt;dbl&amp;gt; &amp;lt;chr&amp;gt;           &amp;lt;chr&amp;gt;       
#&amp;gt; 1 0033s000012… KEEP                  1000 Steven Mortimer 0013s00000z…
#&amp;gt; 2 0033s000012… KEEP                  1000 Steven Mortimer 0013s00000z…
#&amp;gt; 3 0033s000012… KEEP                  1000 Steven Mortimer 0013s00000z…
#&amp;gt; # … with 3 more variables: `Account Name` &amp;lt;chr&amp;gt;, `Billing City` &amp;lt;chr&amp;gt;, `Account
#&amp;gt; #   Owner` &amp;lt;chr&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For more detail on how to take advantage of this new functionality please see
the pkgdown website &lt;a target=&#34;_blank&#34;
href=&#34;https://stevenmmortimer.github.io/salesforcer&#34;&gt;https://stevenmmortimer.github.io/salesforcer&lt;/a&gt;
and, more specifically, the &lt;a target=&#34;_blank&#34;
href=&#34;https://stevenmmortimer.github.io/salesforcer/articles/working-with-reports.html&#34;&gt;Working
with Reports vignette&lt;/a&gt; which provides a soft introduction to these concepts.
Finally, keep an eye out as more dashboard and analytics notifications
functionality is also added.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;support-for-bulk-2.0-api-queries&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Support for Bulk 2.0 API Queries&lt;/h2&gt;
&lt;p&gt;In Salesforce version 47.0 (Winter ’20) query functionality added to
the Bulk 2.0 API. In the overview of this feature Salesforce emphasizes the
consistency with the REST APIs and the ease of use (e.g. “Automatic File
Batching”), but does not mention any claims in terms of speed compared to the
Bulk 1.0 query functionality. In {salesforcer 0.2.2} the default API when using
&lt;code&gt;sf_run_bulk_query()&lt;/code&gt; or &lt;code&gt;sf_query_bulk()&lt;/code&gt; is now the Bulk 2.0 API, assuming it
is better than the Bulk 1.0 API. However, You can easily switch between the APIs
just as you did before in previous {salesforcer} releases by specifying it in the
&lt;code&gt;api_type&lt;/code&gt; argument. Please note that, because of additional standardization on
the column ordering and arguments to guess types, the queries below will all
return the same exact format of results. For example we prioritize the following
fields in queries alphabetically within this prioritization waterfall:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;First, the &lt;code&gt;sObject&lt;/code&gt; field (indicates the record’s object if multiple objects returned in the results)&lt;/li&gt;
&lt;li&gt;Second, the Id field (&lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;sf__Id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Third, record success status (&lt;code&gt;Success&lt;/code&gt;, &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;sf_Success&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fourth, record created status (&lt;code&gt;Created&lt;/code&gt;, &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;sf__Created&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Fifth, record error(s) status (&lt;code&gt;Error&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;errors&lt;/code&gt;,
&lt;code&gt;errors.statusCode&lt;/code&gt;, &lt;code&gt;errors.fields&lt;/code&gt;, &lt;code&gt;errors.message&lt;/code&gt;, &lt;code&gt;sf__Error&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sixth, all other fields from the target object (e.g. &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Phone&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;Seventh, relationship fields (fields from a parent or child of the target). For example,
anything typically containing a dot like &lt;code&gt;Account.Id&lt;/code&gt;, &lt;code&gt;Owner.Name&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In short, Bulk 2.0 now has query functionality and it is consistent with the
other API’s query functionality. I recommend checking to see for yourself which
API works well. Below is a simple example comparing a single run of the REST,
Bulk 1.0, and Bulk 2.0 APIs. Consider using the {microbenchmark} package to run
more precise performance tests.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;soql &amp;lt;- &amp;quot;SELECT Id, LastName, Account.Id, Account.Name, Owner.Id
         FROM Contact
         LIMIT 1000&amp;quot;
system.time(sf_query(soql, api_type = &amp;quot;REST&amp;quot;))
#&amp;gt;    user  system elapsed 
#&amp;gt;   4.033   0.032   5.065
system.time(sf_query(soql, object_name = &amp;quot;Contact&amp;quot;, api_type = &amp;quot;Bulk 1.0&amp;quot;))
#&amp;gt;    user  system elapsed 
#&amp;gt;   0.186   0.018   8.345
system.time(sf_query(soql, api_type = &amp;quot;Bulk 2.0&amp;quot;))
#&amp;gt;    user  system elapsed 
#&amp;gt;   0.046   0.009   7.541&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;support-for-relationship-queries&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Support for Relationship Queries&lt;/h2&gt;
&lt;p&gt;One upgrade for {salesforcer 0.2.2} is better support for relationship queries,
both child-to-parent lookups using the dot notation and parent-to-child nested
queries. In prior releases the results were not parsed consistently and presented
themselves in a variety of hard to debug issues on GitHub that were brought up in &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/salesforcer/issues/19&#34;&gt;#19&lt;/a&gt;, &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/salesforcer/issues/35&#34;&gt;#35&lt;/a&gt;, &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;  href=&#34;https://github.com/StevenMMortimer/salesforcer/issues/38&#34;&gt;#38&lt;/a&gt;, and &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34;  href=&#34;https://github.com/StevenMMortimer/salesforcer/issues/54&#34;&gt;#54&lt;/a&gt;. This
release finally aims to address some of those bugs through more consistent parsing methods
for both the XML returned by the SOAP API and the JSON returned by the REST API. However,
I would strongly recommend testing in your Org with your own queries to see the
impact before deploying to a production environment. If any unexpected behavior
crops up, then please file an issue on GitHub using the &lt;a href=&#34;https://github.com/StevenMMortimer/salesforcer/issues/new?assignees=StevenMMortimer&amp;amp;labels=&amp;amp;template=query-issue-template.md&amp;amp;title=&#34;&gt;query issue template&lt;/a&gt; so we can get it resolved.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Old Nested Query Behavior (v0.1.4 or earlier)&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sf_query(&amp;quot;SELECT Name, (SELECT LastName FROM Contacts) FROM Account&amp;quot;, api_type=&amp;quot;SOAP&amp;quot;)
#&amp;gt; # A tibble: 24 x 4
#&amp;gt;    Id    Name                        Contacts         LastName
#&amp;gt;    &amp;lt;lgl&amp;gt; &amp;lt;chr&amp;gt;                       &amp;lt;list&amp;gt;           &amp;lt;chr&amp;gt;   
#&amp;gt;  1 NA    GenePoint                   &amp;lt;named list [4]&amp;gt; &amp;lt;NA&amp;gt;    
#&amp;gt;  2 NA    &amp;lt;NA&amp;gt;                        &amp;lt;NULL&amp;gt;           Frank   
#&amp;gt;  3 NA    United Oil &amp;amp; Gas, UK        &amp;lt;named list [4]&amp;gt; &amp;lt;NA&amp;gt;    
#&amp;gt;  4 NA    &amp;lt;NA&amp;gt;                        &amp;lt;NULL&amp;gt;           James   
#&amp;gt;  5 NA    United Oil &amp;amp; Gas, Singapore &amp;lt;named list [5]&amp;gt; &amp;lt;NA&amp;gt; 
#&amp;gt; # … with 19 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;New Query Behavior (v0.2.2)&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;sf_query(&amp;quot;SELECT Name, (SELECT LastName FROM Contacts) FROM Account&amp;quot;, api_type=&amp;quot;SOAP&amp;quot;)
#&amp;gt; # A tibble: 16 x 4
#&amp;gt;    Name                        Contact.LastName
#&amp;gt;    &amp;lt;chr&amp;gt;                       &amp;lt;chr&amp;gt;
#&amp;gt;  1 GenePoint                   Frank
#&amp;gt;  2 United Oil &amp;amp; Gas, UK        James
#&amp;gt;  3 United Oil &amp;amp; Gas, Singapore D&amp;#39;Cruz
#&amp;gt;  4 United Oil &amp;amp; Gas, Singapore Ripley
#&amp;gt;  5 Edge Communications         Forbes
#&amp;gt; # … with 11 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new vignette has been included with this release that covers the types of
queries currently supported by the package and is available here: &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/articles/supported-queries.html&#34;&gt;Supported
Queries&lt;/a&gt;. I highly recommend reviewing for guidance and inspiration on how to
what types of queries are possible running against your Org.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;additional-information&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Additional Information&lt;/h2&gt;
&lt;p&gt;For a complete listing of all changes made in recent releases of {salesforcer} please
view the &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/news/index.html#salesforcer-0-2-2-release&#34;&gt;Changelog (aka NEWS.md)&lt;/a&gt; file. Bug reports and feature requests are welcome on GitHub in the repository &lt;a rel=&#34;noopener noreferrer&#34; target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/salesforcer/issues&#34;&gt;issues&lt;/a&gt; section.&lt;/p&gt;
&lt;p&gt;Thank you for your continued support!&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Running GitHub Actions Sequentially</title>
      <link>https://stevenmortimer.com/running-github-actions-sequentially/</link>
      <pubDate>Sun, 26 Jul 2020 20:05:58 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/running-github-actions-sequentially/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/running-github-actions-sequentially/github-actions-logo.png&#34; style=&#34;display:block;margin:auto;max-height:300px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: If you need sequential execution in GitHub Actions consider these solutions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;b&gt;Sequential &lt;u&gt;steps&lt;/u&gt;&lt;/b&gt;: Steps within a job are always executed sequentially!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;b&gt;Sequential &lt;u&gt;jobs&lt;/u&gt;&lt;/b&gt;: Set &lt;code&gt;max-parallel: 1&lt;/code&gt; within the &lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel&#34;&gt;&lt;code&gt;jobs.strategy&lt;/code&gt;&lt;/a&gt; element of the workflow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;b&gt;Sequential &lt;u&gt;workflows&lt;/u&gt;&lt;/b&gt;: Use a &lt;code&gt;repository_dispatch&lt;/code&gt; API call at the end of the workflow to trigger the next workflow (code available in the &lt;a href=&#34;#an-example&#34;&gt;An Example&lt;/a&gt; section below).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;div id=&#34;the-issue&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;The issue&lt;/h2&gt;
&lt;p&gt;Ever since the generally available release of GitHub Actions in November (2019),
it seems like many R packages developed on GitHub have switched from Travis CI or
another continuous integration service to now using GitHub Actions (GHA). It seems
like a great service to have integrated as closely as possible to your codebase,
but the product is still under active development. There is a dedicated
&lt;a target=&#34;_blank&#34; href=&#34;https://github.community/c/github-actions&#34;&gt;tag&lt;/a&gt; in
the GitHub Support Community to ask questions and browse answers. However, in a
few different questions (&lt;a target=&#34;_blank&#34;
href=&#34;https://github.community/t/triggering-a-new-workflow-from-another-workflow/16250&#34;&gt;here&lt;/a&gt;, &lt;a target=&#34;_blank&#34;
href=&#34;https://github.community/t/run-next-job-sequentially-even-if-a-previous-job-fails/17404&#34;&gt;here&lt;/a&gt;, and &lt;a target=&#34;_blank&#34;
href=&#34;https://github.community/t/steps-in-parallel/16343&#34;&gt;here&lt;/a&gt;) it seems like folks are still grappling
with GitHub’s design decision to execute all workflows and jobs in a repository
in parallel, myself included.&lt;/p&gt;
&lt;p&gt;As background, it is important to note here the basic lingo of GitHub Actions. A
repository can contain one or more ‘workflows’ defined by YAML files located in
the &lt;code&gt;.github/workflows&lt;/code&gt; folder in the top level of the repository. Each
‘workflow’ can contain one or more ‘jobs’ that execute a series of ‘steps’.
By default, all steps in a single job execute sequentially. If you’re trying to
limit the number of parallel ‘jobs’ then you you can set a limit of 1 for the
workflow by setting &lt;code&gt;max-parallel: 1&lt;/code&gt; within the &lt;a target=&#34;_blank&#34;
href=&#34;https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymax-parallel&#34;&gt;&lt;code&gt;jobs.strategy&lt;/code&gt;&lt;/a&gt;
element of the workflow YAML. However, the issue with multiple jobs in a single
workflow is that if one job fails out of 10 jobs in your workflow, then
you’ll have to re-run all 10 jobs for the workflow status to be a success. For
this reason, I have decided to split jobs across different workflows. This way,
I can re-run one individually, if needed. I will refer to ‘jobs’ and
‘workflows’ interchangeably throughout this article because you can have workflows
that execute one job and one job only.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do I need sequential workflows/jobs?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The reason I need this functionality is that my workflows interact with a 3rd-party
service (Salesforce) and one workflow might affect the results of another
workflow if accessing the service simultaneously. I also want to prevent other
workflows from executing if I can’t get the first one to succeed since the issue
could occur across all workflows. This allows me to reduce the total number
of API calls to Salesforce which are capped in a 24-hour period by catching issues
early before other workflows execute.&lt;/p&gt;
&lt;hr /&gt;
&lt;/div&gt;
&lt;div id=&#34;an-example&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;An Example&lt;/h2&gt;
&lt;p&gt;You can setup sequential workflows using a &lt;code&gt;repository_dispatch&lt;/code&gt; action in 4
easy steps:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;&lt;a href=&#34;#step-1---create-a-personal-access-token-pat&#34;&gt;Step 1 - Create a Personal Access Token (PAT)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-2---add-the-pat-as-an-actions-secret-in-the-repository&#34;&gt;Step 2 - Add the PAT as an actions secret in the repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-3---add-the-repository_dispatch-event-to-workflow-1&#34;&gt;Step 3 - Add the repository_dispatch event to Workflow 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-4---add-the-repository_dispatch-event-as-trigger-in-workflow-2-yaml&#34;&gt;Step 4 - Add the repository_dispatch event as trigger in Workflow 2 YAML&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For context, a required element in every workflow the name of the
GitHub event that triggers the workflow. For example, &lt;code&gt;on: pull_request&lt;/code&gt; means
“execute this workflow every time a pull request is opened”. If you want to run
workflows sequentially, then you just need to issue a specific event type that
lets the next workflow know when to begin. You could try to write your own solution
that uses the GitHub Actions APIs to list the workflows, jobs, or check-suites
and find out which ones have failed or not, but the easy alternative is to use a
&lt;code&gt;repository_dispatch&lt;/code&gt; event.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“You can use this endpoint to trigger a webhook event called repository_dispatch when you want activity that happens outside of GitHub to trigger a GitHub Actions workflow or GitHub App webhook.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;sup&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://developer.github.com/v3/repos/#create-a-repository-dispatch-event&#34;&gt;https://developer.github.com/v3/repos/#create-a-repository-dispatch-event&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Workflows aren’t aware of other workflows, so this event webhook is perfect to trigger,
or “daisy-chain”, separate workflows. You could execute the event via a &lt;code&gt;curl&lt;/code&gt; command
from the shell in a new job step, but I recommend using the &lt;a target=&#34;_blank&#34;
href=&#34;https://github.com/marketplace/actions/repository-dispatch&#34;&gt;&lt;i&gt;Repository
Dispatch&lt;/i&gt;&lt;/a&gt; action that Peter Evans has released in the GitHub Marketplace,
which makes it dirt simple to execute a repository dispatch event from a workflow.&lt;/p&gt;
&lt;div id=&#34;step-1---create-a-personal-access-token-pat&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 1 - Create a Personal Access Token (PAT)&lt;/h3&gt;
&lt;p&gt;Follow GitHub’s instructions &lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token&#34;&gt;here&lt;/a&gt; and
when it comes time to select the scopes, or permissions, you’d like to grant the
token then check &lt;code&gt;&#34;repo&#34;&lt;/code&gt; if you’re on a private repository or &lt;code&gt;&#34;public_repo&#34;&lt;/code&gt; if
you’re on a public one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/running-github-actions-sequentially/new-pat.png&#34; style=&#34;display:block;margin:auto;max-width:500px;&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-2---add-the-pat-as-an-actions-secret-in-the-repository&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 2 - Add the PAT as an actions secret in the repository&lt;/h3&gt;
&lt;p&gt;Follow GitHub’s instructions &lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets-for-a-repository&#34;&gt;here&lt;/a&gt;. I recommend naming the secret &lt;code&gt;REPO_GHA_PAT&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/running-github-actions-sequentially/new-secret.png&#34; style=&#34;display:block;margin:auto;max-width:500px;&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-3---add-the-repository_dispatch-event-to-workflow-1&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 3 - Add the &lt;code&gt;repository_dispatch&lt;/code&gt; event to Workflow 1&lt;/h3&gt;
&lt;p&gt;This step is where you update Workflow 1’s YAML file. For this example, consider
“Workflow 1” as the workflow, and the job(s) contained within it, as what you’d
like to execute first. Think of “Workflow 2” as the workflow you’d like to
execute after “Workflow 1”. Add the following as the last step in the workflow YAML
file:&lt;/p&gt;
&lt;pre class=&#34;yaml&#34;&gt;&lt;code&gt;- name: Trigger next workflow
  if: success()
  uses: peter-evans/repository-dispatch@v1
  with:
    token: ${{ secrets.REPO_GHA_PAT }}
    repository: ${{ github.repository }}
    event-type: trigger-workflow-2
    client-payload: &amp;#39;{&amp;quot;ref&amp;quot;: &amp;quot;${{ github.ref }}&amp;quot;, &amp;quot;sha&amp;quot;: &amp;quot;${{ github.sha }}&amp;quot;}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example above, you’ll notice the line &lt;code&gt;if: success()&lt;/code&gt;. This means that, only
if all the prior steps in the workflow were successful, we should run this step
that triggers Workflow 2. Also you should notice the line which passes data from
Workflow 1 to Workflow 2:&lt;/p&gt;
&lt;pre class=&#34;javascript&#34;&gt;&lt;code&gt;client-payload: &amp;#39;{&amp;quot;ref&amp;quot;: &amp;quot;${{ github.ref }}&amp;quot;, &amp;quot;sha&amp;quot;: &amp;quot;${{ github.sha }}&amp;quot;}&amp;#39;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case it is telling Workflow 2 the branch and commit hash to checkout and
use so that we know Workflows 1 and 2 are using the same exact code. Remember,
it’s possible that you have a couple different Workflow 1’s running because
you’ve pushed code or triggered them in some way and you want to make sure each
one triggers the same code in Workflow 2.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-4---add-the-repository_dispatch-event-as-trigger-in-workflow-2-yaml&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 4 - Add the &lt;code&gt;repository_dispatch&lt;/code&gt; event as trigger in Workflow 2 YAML&lt;/h3&gt;
&lt;p&gt;This step is where you update Workflow 2’s YAML file. First, add your event name as
the type of repository dispatch that should trigger Workflow 2. This name
must match exactly as what you specified the &lt;code&gt;event-type&lt;/code&gt; we covered in Step 3
(the last step of Workflow 1). In our case we called the event to trigger Workflow 2
as &lt;code&gt;event-type: trigger-workflow-2&lt;/code&gt;. This could be called anything you wish. The
important part is using the same name in the &lt;code&gt;types&lt;/code&gt; key of the Workflow 2 YAML
file. It should be included within square brackets and without quotes as shown below:&lt;/p&gt;
&lt;pre class=&#34;yaml&#34;&gt;&lt;code&gt;name: Workflow 2

on:
  repository_dispatch:
    types: [trigger-workflow-2]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Second, use the client payload data from the event to checkout the same code. You
can so this by modifying the checkout step, usually one of the first step in your job.&lt;/p&gt;
&lt;pre class=&#34;yaml&#34;&gt;&lt;code&gt;    steps:
      - uses: actions/checkout@v2
        with:
          ref: ${{ github.event.client_payload.sha }}
      
      ... (other steps)
      
      - uses: r-lib/actions/setup-r@master
      
      - uses: r-lib/actions/setup-pandoc@master&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it! If you have more than 2 workflows, then simply add a unique trigger as
the last step in each workflow that calls the next. For the first workflow, I typically
trigger based on a push to a certain branch like this:&lt;/p&gt;
&lt;pre class=&#34;yaml&#34;&gt;&lt;code&gt;name: Workflow 1

on:
  push:
    branches: main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/br&gt;&lt;img alt=&#34;GitHub - salesforcer GHA workflows&#34; style=&#34;display:inline-block;float:left;vertical-align:text-top;margin-right:10px;margin-left:0px;margin-top:0px;margin-bottom:0px;&#34; src=&#34;https://stevenmortimer.com/blog/running-github-actions-sequentially/github-logo.png&#34; /&gt; If you would like to see a complete example in action, then feel free to browse the &lt;a target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/salesforcer/tree/main/.github/workflows&#34;&gt;&lt;code&gt;.github/workflows&lt;/code&gt;&lt;/a&gt; folder
for the {salesforcer} package.&lt;/p&gt;
&lt;hr /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;considerations&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Considerations&lt;/h2&gt;
&lt;p&gt;If you need your jobs to execute sequentially but you want them to all still
run, even if some fail, then just change the &lt;code&gt;if:&lt;/code&gt; statement mentioned above in Step 3
to &lt;code&gt;if: always()&lt;/code&gt;. The only reason I did not do this is that even if the following
workflow is successful, it will get triggered again when we re-run the failed workflow,
which I didn’t want. In order to achieve that you may need to use some more
advanced tricks/hacks to only execute if the next workflow has the same &lt;code&gt;sha&lt;/code&gt;
&lt;b&gt;&lt;u&gt;and&lt;/u&gt;&lt;/b&gt; the latest run does not have a ‘completed’ status.&lt;/p&gt;
&lt;p&gt;Another consideration is the cost to execute GitHub actions in private repositories.
It’s true that many projects will not likely need to enforce sequential workflows
because the tests, examples, checks or dependencies do not affect other
workflows. However, GitHub Actions is only free for public respositories. You
may have private repositories and want to limit the amount of processing time so
you can stay within the free tier (less than 2,000 minutes per month).
Sequential execution can prevent all the workflows from executing if upstream
workflows fail.&lt;/p&gt;
&lt;hr /&gt;
&lt;/div&gt;
&lt;div id=&#34;references&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;p&gt;You may find the following resources helpful when setting up on your own repository:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://github.com/marketplace/actions/repository-dispatch&#34;&gt;Peter Evans’ GitHub Action to create a &lt;code&gt;respository_dispatch&lt;/code&gt; API call&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/actions/getting-started-with-github-actions&#34;&gt;Getting Started with GitHub Actions&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/rest/guides/getting-started-with-the-rest-api&#34;&gt;GitHub Actions REST API Documentation&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://github.community/c/github-actions/41&#34;&gt;GitHub Support Community for Actions&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>5 steps to change GitHub default branch from master to main</title>
      <link>https://stevenmortimer.com/5-steps-to-change-github-default-branch-from-master-to-main/</link>
      <pubDate>Thu, 23 Jul 2020 12:37:37 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/5-steps-to-change-github-default-branch-from-master-to-main/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/master-to-main.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The R community is a diverse and inclusive group. About a month ago (June 2020)
GitHub &lt;a target=&#34;_blank&#34; href=&#34;https://www.vice.com/en_us/article/k7qbyv/github-to-remove-masterslave-terminology-from-its-platform&#34;&gt;announced&lt;/a&gt; it would start to remove references to ‘master’ across its
sites and I’m surprised the R community hasn’t jumped on the opportunity to
be on the bleeding edge of this change. I think we can all take a very quick and
very small step forward in removing divisive language in tech by changing the default
branch name in R package repositories to ‘main’ instead of ‘master’. There are a
wealth of other posts/tutorials that others have written. Here is a small sampling
listed in order of publishing date:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://www.hanselman.com/blog/EasilyRenameYourGitDefaultBranchFromMasterToMain.aspx&#34;&gt;&lt;i&gt;Easily rename your Git default branch from master to main&lt;/i&gt;&lt;/a&gt; by Scott Hanselman&lt;/li&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://dev.to/rhymu8354/git-renaming-the-master-branch-137b&#34;&gt;&lt;i&gt;Git: Renaming the “master” branch&lt;/i&gt;&lt;/a&gt; by Richard Walters&lt;/li&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://jarv.is/notes/github-rename-master/&#34;&gt;&lt;i&gt;How To: Safely Rename master Branch on GitHub&lt;/i&gt;&lt;/a&gt; by Jake Jarvis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Eugene Shen even made a small web app to automate the entire process for you that is
available at: (&lt;a target=&#34;_blank&#34; href=&#34;https://eyqs.ca/tools/rename/&#34;&gt;https://eyqs.ca/tools/rename/&lt;/a&gt;).
I thought I’d share the 5 simple steps that I tested and used to make the change
in &lt;strong&gt;under 1 minute&lt;/strong&gt;. Yes, you can do this in under a minute, so what’s stopping
you? If you are concerned about compatibility, GitHub is providing updates on their
renaming efforts here: &lt;a target=&#34;_blank&#34; href=&#34;https://github.com/github/renaming&#34;&gt;https://github.com/github/renaming&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Below are screenshots to walk you through each step. In addition, there is a block
of code with all Terminal commands in one place at the end of this post
&lt;a href=&#34;#all-commands&#34;&gt;here&lt;/a&gt; and GitHub repo here:
&lt;a target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/master-to-main&#34;&gt;https://github.com/StevenMMortimer/master-to-main&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;div id=&#34;steps&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Steps&lt;/h2&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;&lt;a href=&#34;#step-1---move-the-master-branch-to-main&#34;&gt;Step 1 - Move the master branch to ‘main’&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-2---push-main-to-remote-repo&#34;&gt;Step 2 - Push ‘main’ to remote repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-3---point-head-to-main-branch&#34;&gt;Step 3 - Point HEAD to ‘main’ branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-4---change-default-branch-to-main-on-github-site&#34;&gt;Step 4 - Change default branch to ‘main’ on GitHub site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-5---delete-master-branch-on-the-remote-repo&#34;&gt;Step 5 - Delete ‘master’ branch on the remote repo&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div id=&#34;step-1---move-the-master-branch-to-main&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Step 1 - Move the ‘master’ branch to ‘main’&lt;/h2&gt;
&lt;p&gt;Run the following command which creates a branch called ‘main’ using the history
from ‘master’. Using the argument &lt;code&gt;-m&lt;/code&gt; will transfer all of the commit history on
the ‘master’ branch to your new ‘main’ branch so nothing gets lost.&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git branch -m master main&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;step-2---push-main-to-remote-repo&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Step 2 - Push ‘main’ to remote repo&lt;/h2&gt;
&lt;p&gt;Remember that git is version control software on your local machine and GitHub
is the remote server that stores your code. For this reason, you’ll have to push
your new ‘main’ branch up to GitHub and tell the local branch to start tracking
the remote branch with the same name.&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git push -u origin main&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;step-3---point-head-to-main-branch&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Step 3 - Point HEAD to ‘main’ branch&lt;/h2&gt;
&lt;p&gt;At this stage if ‘master’ was your default branch you cannot remove it without
first changing HEAD, the pointer to the current branch reference. This following
command will point it to ‘main’.&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All three steps are shown in the screenshot below:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/steps-1-to-3-create-main-and-point-HEAD.png&#34; style=&#34;display:block;max-height:300px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;At this point you can take a breather. You’re on the home stretch! If you want
to check that things are going as planned then you’re welcome to run the following
that should show the HEAD is pointing to main which now frees you up to delete ‘master’.
&lt;strong&gt;Note&lt;/strong&gt;: When you enter this command in your Terminal you will have to type &lt;code&gt;:q&lt;/code&gt; to
exit it. Not &lt;code&gt;CTRL+C&lt;/code&gt;, &lt;code&gt;ESC&lt;/code&gt;, etc.&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git branch -a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/steps-1-to-3-review.png&#34; style=&#34;display:block;max-height:100px;&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-4---change-default-branch-to-main-on-github-site&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Step 4 - Change default branch to ‘main’ on GitHub site&lt;/h2&gt;
&lt;p&gt;At this point you’ve succesfully transitioned everything to the ‘main’ branch, but
you can’t delete the ‘master’ branch without changing the default branch in GitHub
to something other than ‘master’. This is the only step that requires you to leave
the Terminal and navigate in your browser to your GitHub repository. From there,
click &lt;code&gt;&#34;Settings&#34; -&amp;gt; &#34;Branches&#34;&lt;/code&gt; on the left rail and change the default branch to
‘main’. I’ve included some screenshots below and GitHub provides instructions for
doing this here: &lt;a target=&#34;_blank&#34; href=&#34;https://docs.github.com/en/github/administering-a-repository/setting-the-default-branch&#34;&gt;https://docs.github.com/en/github/administering-a-repository/setting-the-default-branch&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/step-4a-go-to-settings.png&#34; style=&#34;display:block;max-height:300px;&#34; /&gt;&lt;/br&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/step-4b-select-main-as-default.png&#34; style=&#34;display:block;max-height:300px;&#34; /&gt;&lt;/br&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/step-4c-confirm-change.png&#34; style=&#34;display:block;max-height:300px;&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-5---delete-master-branch-on-the-remote-repo&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Step 5 - Delete ‘master’ branch on the remote repo&lt;/h2&gt;
&lt;p&gt;Now that the local ‘master’ is gone and there’s nothing pointing to it on Github
you can delete it with the following command:&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git push origin --delete master&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/step-5-delete-remote-branch.png&#34; style=&#34;display:block;max-height:75px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;That’s it! You’re done! You should no longer see “master” or “remotes/origin/master” locally
or on your GitHub repository. If you want to check, you can always run the following:&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;git branch -a&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/5-steps-to-change-github-default-branch-from-master-to-main/final-check.png&#34; style=&#34;display:block;max-height:80px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Feel free to let me know if this worked for you or if you had issues! &lt;strong&gt;Note&lt;/strong&gt;:
I take no responsibility for messing up your git repo. In case this wasn’t clear
enough, all instructions are available in its own repo here: &lt;a target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/master-to-main&#34;&gt;https://github.com/StevenMMortimer/master-to-main&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;/div&gt;
&lt;div id=&#34;all-commands&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;All commands&lt;/h2&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;# Step 1 
# create main branch locally, taking the history from master
git branch -m master main

# Step 2 
# push the new local main branch to the remote repo (GitHub) 
git push -u origin main

# Step 3
# switch the current HEAD to the main branch
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main

# Step 4
# change the default branch on GitHub to main
# https://docs.github.com/en/github/administering-a-repository/setting-the-default-branch

# Step 5
# delete the master branch on the remote
git push origin --delete master&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Setting up VS Code for Python Development like RStudio</title>
      <link>https://stevenmortimer.com/setting-up-vs-code-for-python-development-like-rstudio/</link>
      <pubDate>Tue, 14 Jul 2020 18:45:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/setting-up-vs-code-for-python-development-like-rstudio/</guid>
      <description>
        


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/vscode-screenshot.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;In this article I will highlight the features of VS Code that match RStudio
exactly, such as the “interactive notebook window” (called the Console in R) or
the “variable explorer” (like running &lt;code&gt;View()&lt;/code&gt; on a data frame in RStudio). At
the bottom of this post I will provide two JSON files (&lt;code&gt;settings.json&lt;/code&gt; and
&lt;code&gt;keybindings.json&lt;/code&gt;) and a block of code to install from the command line a list
of extensions that I recommend. By using these files as a guide you can
configure your VS Code installation to do a pretty good job at mimicking
RStudio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First, why try to write Python like you write R code in RStudio??&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;RStudio is a great all around IDE for data analysis. A few years ago I was
transitioning from writing a lot of R code to more Python code at work. I
initially chose PyCharm as my Python IDE for a variety of reasons outlined in
another blog post of mine: &lt;a target=&#34;_blank&#34;
href=&#34;https://stevenmortimer.com/an-r-user-chooses-a-python-ide/&#34;&gt;An R User
Chooses a Python IDE&lt;/a&gt;. However, as of last summer (June 2019), I switched to
VS Code as a Python IDE and never looked back. In hindsight, PyCharm just seems
too clunky with an over-engineered GUI of buttons to click and not really be
sure what’s going on. VS Code is making great strides towards becoming an IDE
that works well for the REPL (read–eval–print loop) style of coding that RStudio
excels at supporting. I love how lightweight VS Code feels and how the
configurations are portable via JSON files making it easier to share a common
config with team members. I’ll keep writing R code in RStudio, but VS Code is
quckly becoming a second home for me to write Python code.&lt;/p&gt;
&lt;div id=&#34;how-to-configure-vs-code-to-run-like-rstudio&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;How to Configure VS Code to run like RStudio&lt;/h2&gt;
&lt;div id=&#34;aesthetics-textmate-theme-and-margin-column-ruler&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Aesthetics (Textmate theme and margin column ruler)&lt;/h3&gt;
&lt;p&gt;You may know that the default theme in RStudio is “Textmate”. This is found in
&lt;code&gt;Tools -&amp;gt; Global Options -&amp;gt; Appearance&lt;/code&gt;. To get the color scheme in VS Code you
can install this with the &lt;a target=&#34;_blank&#34;
href=&#34;https://marketplace.visualstudio.com/items?itemName=gerane.Theme-textmate&#34;&gt;Textmate theme extension&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/textmate-theme.png&#34; style=&#34;display:block;margin:auto;max-height:300px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;I tend to not change much beyond the default RStudio installation just so that I
don’t need to maintain a unique and unfamiliar setup compared to other
colleagues who are also using RStudio. The only customization I recommend is
adding a vertical line at 80 characters. This is a must-have feature to keep
your code readable and you can do the same with VS Code by specifying
&lt;code&gt;&#34;editor.rulers&#34;&lt;/code&gt; in your user settings. More detail on how to configure that is in
the section of this post entitled &lt;a href=&#34;#settings-json-file&#34;&gt;Settings JSON File&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;running-code&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Running Code&lt;/h3&gt;
&lt;p&gt;As far as running code in RStudio, it is fairly common to write code in the
“Source” pane (normally above the console), then send the code to the “Console”
pane to run using &lt;code&gt;CMD+ENTER&lt;/code&gt; (&lt;code&gt;CRTL+ENTER&lt;/code&gt; if on Windows - please assume
anywhere I refer to &lt;code&gt;CMD&lt;/code&gt; in this article it is &lt;code&gt;CTRL&lt;/code&gt; if you use Windows). In
VS Code you can think of the Editor pane as having the exact same purpose
(writing a script), but instead of sourcing lines to the “Console” you use the same
command (&lt;code&gt;CMD+ENTER&lt;/code&gt;) to run the code in the Python Interactive Window. More
specifically, the keyboard shortcut you need to set in VS Code is for the
command &lt;code&gt;&#34;python.datascience.execSelectionInteractive&#34;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a game changer when writing Python code for analysis because you no
longer need to code in a Jupyter Notebook to execute your analysis. Simply write
your code in a &lt;code&gt;.py&lt;/code&gt; file and press &lt;code&gt;CMD+ENTER&lt;/code&gt; to execute line-by-line in the
Python Interactive Window. Repeat this process as you run code, explore, and
build out your analysis. Note that you can also type Python directly into the
Interactive Window just like you can type directly in R’s Console as well to
execute code.&lt;/p&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/interactive-window-intellisense.gif&#34; /&gt;
&lt;div style=&#34;text-align:center&#34;&gt;
&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/python/jupyter-support-py#_python-interactive-window&#34;&gt;https://code.visualstudio.com/docs/python/jupyter-support-py#_python-interactive-window&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;variable-explorer-environment-tab-in-r&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Variable Explorer (“Environment” tab in R)&lt;/h3&gt;
&lt;p&gt;Notebooks always seem clunky in terms of executing single lines of code and
reviewing variables in the global environment, but VS Code has a variable
explorer just like the “Environment” tab in R.&lt;/p&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/variable-explorer.png&#34; /&gt;
&lt;div style=&#34;text-align:center&#34;&gt;
&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/python/jupyter-support-py#_variable-explorer-and-data-viewer&#34;&gt;https://code.visualstudio.com/docs/python/jupyter-support-py#_variable-explorer-and-data-viewer&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;data-viewer-like-running-view-in-r&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Data Viewer (Like running &lt;code&gt;View()&lt;/code&gt; in R)&lt;/h3&gt;
&lt;p&gt;Once you’ve created a &lt;strong&gt;pandas&lt;/strong&gt; DataFrame you can explore a dynamic view of the
data frame with basic filtering and sorting. I have found this be exactly like
the &lt;code&gt;View()&lt;/code&gt; function in RStudio. The only difference is it does seem to
struggle a bit when viewing data frames over 1 million rows. It is laggy or
crashes VS Code, but I don’t find that too much of a problem because you can
always save a sample of your dataset into a variable or aggregate your data
prior to viewing.&lt;/p&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/data-viewer.png&#34; /&gt;
&lt;div style=&#34;text-align:center&#34;&gt;
&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/python/jupyter-support-py#_variable-explorer-and-data-viewer&#34;&gt;https://code.visualstudio.com/docs/python/jupyter-support-py#_variable-explorer-and-data-viewer&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;plot-viewer-plots-tab-in-r&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Plot Viewer (“Plots” tab in R)&lt;/h3&gt;
&lt;p&gt;You can flip through prior plots, save them, resize, and interact with plots the
same way as you would in the “Plots” pane of RStudio. Nothing different here.&lt;/p&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/plot-viewer.gif&#34; /&gt;
&lt;div style=&#34;text-align:center&#34;&gt;
&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/python/jupyter-support-py#_plot-viewer&#34;&gt;https://code.visualstudio.com/docs/python/jupyter-support-py#_plot-viewer&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;version-control-git-tab-in-r&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Version Control (“Git” tab in R)&lt;/h3&gt;
&lt;p&gt;Just like RStudio you can stage files, write a commit message, and push to a git
repository. Instead of ticking a checkbox to stage files, you will have to press
the &lt;code&gt;+&lt;/code&gt; sign, which is about the only difference. Everything else is the similar
as far as clicking on the files to see “diffs”, writing the commit message, and
pushing code to a remote repository.&lt;/p&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/setting-up-vs-code-for-python-development-like-rstudio/git-integration.png&#34; style=&#34;display:block;margin:auto;max-height:200px;&#34; /&gt;
&lt;div style=&#34;text-align:center&#34;&gt;
&lt;p&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/editor/versioncontrol#_git-support&#34;&gt;https://code.visualstudio.com/docs/editor/versioncontrol#_git-support&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;All of these git features can be made even
better with Eric Amodio’s &lt;a target=&#34;_blank&#34;
href=&#34;https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens&#34;&gt;GitLens
extension&lt;/a&gt;, which I highly recommend. It makes it easier to navigate
branches, see when files were changed and by whom.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;whats-missing-or-different-in-vs-code&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;What’s missing or different in VS Code?&lt;/h2&gt;
&lt;div id=&#34;rs-document-outline-aka-minimap-in-vs-code&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;R’s Document Outline (aka Minimap in VS Code)&lt;/h3&gt;
&lt;p&gt;RStudio creates a nice outline of your scripts based on your functions and
comments that start with:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# A new section ----
code here...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This table of contents style view is helpful. As a somewhat suitable alternative
I have become accustomed to annotating Python code blocks with region folding
syntax which creates collapsible code sections in the VS Code editor.&lt;/p&gt;
&lt;pre class=&#34;py&#34;&gt;&lt;code&gt;# region ----

code here...

# endregion&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;debugging&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Debugging&lt;/h3&gt;
&lt;p&gt;RStudio has a nice feature that will ask you if you want to run a piece of code
with debug mode on if it initially errors. VS Code’s Python Interactive Window
also prints out error messages, but will not let you debug with the click of a
button. You can however set breakpoints and run your script in debug mode, which
is a familiar experience, just not as seamless as RStudio.&lt;/p&gt;
&lt;p&gt;There are obviously many other features, like having a built-in terminal, remote
connections, app development via Shiny, etc. that make R/RStudio and Python/VS
Code different tools, but if you’re just running analysis the two can provide
very similar workflows, right down to the keyboard shortcuts. The only challenge
you may have left is figuring out how to make your &lt;strong&gt;pandas&lt;/strong&gt; code as legible and
well organized as &lt;strong&gt;dplyr&lt;/strong&gt; code… but that is for another day. (Hint: It’s possible
with a strong commitment to method chaining).&lt;/p&gt;
&lt;hr /&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;setup-files-and-scripts&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Setup Files and Scripts&lt;/h2&gt;
&lt;div id=&#34;settings-json-file&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Settings JSON File&lt;/h3&gt;
&lt;p&gt;In order to update your &lt;code&gt;settings.json&lt;/code&gt; file, open the Command Palette with
&lt;code&gt;CMD+SHIFT+P&lt;/code&gt; and select &lt;code&gt;&#34;Preferences: Open Settings (JSON)&#34;&lt;/code&gt; to edit the JSON
file where your settings are held. You can copy/paste the entire block of JSON
below or just individual lines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; These settings files were automatically generated from my VS Code
installation using Shan Khan’s &lt;a target=&#34;_blank&#34; href=&#34;https://marketplace.visualstudio.com/items?itemName=Shan.code-settings-sync&#34;&gt;Settings Sync extension&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;settings.json&lt;/strong&gt; (User Settings, as opposed to workspace/project-specific settings)&lt;/p&gt;
&lt;pre class=&#34;json&#34;&gt;&lt;code&gt;{
    &amp;quot;window.zoomLevel&amp;quot;: 1,
    &amp;quot;explorer.confirmDelete&amp;quot;: false,
    &amp;quot;explorer.confirmDragAndDrop&amp;quot;: false,
    &amp;quot;files.associations&amp;quot;: {},
    &amp;quot;files.autoSave&amp;quot;: &amp;quot;off&amp;quot;,
    &amp;quot;files.exclude&amp;quot;: {
        &amp;quot;**/.git&amp;quot;: true,
        &amp;quot;**/.svn&amp;quot;: true,
        &amp;quot;**/.hg&amp;quot;: true,
        &amp;quot;**/CVS&amp;quot;: true,
        &amp;quot;**/.DS_Store&amp;quot;: true,
        &amp;quot;**/.history/**&amp;quot;: true,
        &amp;quot;**/History_**&amp;quot;: true
    },
    &amp;quot;workbench.colorTheme&amp;quot;: &amp;quot;textmate&amp;quot;,
    &amp;quot;workbench.editor.enablePreview&amp;quot;: false,
    &amp;quot;workbench.startupEditor&amp;quot;: &amp;quot;welcomePageInEmptyWorkbench&amp;quot;,
    &amp;quot;terminal.integrated.fontSize&amp;quot;: 14,
    &amp;quot;terminal.integrated.cursorStyle&amp;quot;: &amp;quot;line&amp;quot;,
    &amp;quot;terminal.integrated.copyOnSelection&amp;quot;: true,
    &amp;quot;terminal.integrated.confirmOnExit&amp;quot;: false,
    &amp;quot;editor.largeFileOptimizations&amp;quot;: false,
    &amp;quot;editor.suggest.shareSuggestSelections&amp;quot;: true,
    &amp;quot;editor.suggestSelection&amp;quot;: &amp;quot;first&amp;quot;,
    &amp;quot;editor.minimap.enabled&amp;quot;: false,
    &amp;quot;editor.foldingStrategy&amp;quot;: &amp;quot;indentation&amp;quot;,
    &amp;quot;editor.showFoldingControls&amp;quot;: &amp;quot;always&amp;quot;,
    &amp;quot;editor.rulers&amp;quot;: [
        80
    ],
    &amp;quot;editor.formatOnSave&amp;quot;: true,
    &amp;quot;python.linting.enabled&amp;quot;: true,
    &amp;quot;python.linting.flake8Enabled&amp;quot;: true,
    &amp;quot;python.linting.flake8Args&amp;quot;: [
        &amp;quot;--ignore=E203, E266, E501, W503&amp;quot;,
        &amp;quot;--max-line-length=79&amp;quot;,
        &amp;quot;--select=B,C,E,F,W,T4,B9&amp;quot;,
        &amp;quot;--max-complexity=18&amp;quot;
    ],
    &amp;quot;python.formatting.provider&amp;quot;: &amp;quot;autopep8&amp;quot;,
    &amp;quot;python.formatting.autopep8Args&amp;quot;: [
        &amp;quot;--ignore=E501,E402&amp;quot;
    ],
    &amp;quot;sonarlint.rules&amp;quot;: {
        &amp;quot;python:S3776&amp;quot;: {
            &amp;quot;level&amp;quot;: &amp;quot;off&amp;quot;
        }
    },
    &amp;quot;python.languageServer&amp;quot;: &amp;quot;Microsoft&amp;quot;,
    &amp;quot;python.dataScience.askForKernelRestart&amp;quot;: false,
    &amp;quot;python.dataScience.runStartupCommands&amp;quot;: &amp;quot;%load_ext autoreload\\n%autoreload 2&amp;quot;,
    &amp;quot;python.dataScience.sendSelectionToInteractiveWindow&amp;quot;: true,
    &amp;quot;python.dataScience.useNotebookEditor&amp;quot;: false,
    &amp;quot;git.autofetch&amp;quot;: true,
    &amp;quot;git.confirmSync&amp;quot;: false,
    &amp;quot;gitlens.views.repositories.location&amp;quot;: &amp;quot;gitlens&amp;quot;,
    &amp;quot;gitlens.views.repositories.files.layout&amp;quot;: &amp;quot;list&amp;quot;,
    &amp;quot;gitlens.views.fileHistory.location&amp;quot;: &amp;quot;gitlens&amp;quot;,
    &amp;quot;gitlens.views.lineHistory.enabled&amp;quot;: false,
    &amp;quot;gitlens.views.compare.location&amp;quot;: &amp;quot;gitlens&amp;quot;,
    &amp;quot;gitlens.views.search.location&amp;quot;: &amp;quot;gitlens&amp;quot;,
    &amp;quot;gitlens.mode.statusBar.enabled&amp;quot;: true,
    &amp;quot;gitlens.mode.statusBar.alignment&amp;quot;: &amp;quot;left&amp;quot;,
    &amp;quot;gitlens.currentLine.enabled&amp;quot;: false,
    &amp;quot;gitlens.hovers.enabled&amp;quot;: false,
    &amp;quot;vsintellicode.modify.editor.suggestSelection&amp;quot;: &amp;quot;automaticallyOverrodeDefaultValue&amp;quot;,
    &amp;quot;tabnine.experimentalAutoImports&amp;quot;: true,
    &amp;quot;indentRainbow.errorColor&amp;quot;: &amp;quot;rgba(255,255,255,0.0)&amp;quot;,
    &amp;quot;todo-tree.tree.flat&amp;quot;: true,
    &amp;quot;todo-tree.highlights.defaultHighlight&amp;quot;: {
        &amp;quot;type&amp;quot;: &amp;quot;text-and-comment&amp;quot;,
        &amp;quot;foreground&amp;quot;: &amp;quot;grey&amp;quot;
    },
    &amp;quot;better-comments.tags&amp;quot;: [
        {
            &amp;quot;tag&amp;quot;: &amp;quot;?&amp;quot;,
            &amp;quot;color&amp;quot;: &amp;quot;#8f5785&amp;quot;,
            &amp;quot;strikethrough&amp;quot;: false,
            &amp;quot;backgroundColor&amp;quot;: &amp;quot;transparent&amp;quot;
        }
        {
            &amp;quot;tag&amp;quot;: &amp;quot;*&amp;quot;,
            &amp;quot;color&amp;quot;: &amp;quot;#69a800&amp;quot;,
            &amp;quot;strikethrough&amp;quot;: false,
            &amp;quot;backgroundColor&amp;quot;: &amp;quot;transparent&amp;quot;
        }
    ],
    &amp;quot;bracket-pair-colorizer-2.colors&amp;quot;: [
        &amp;quot;#992e24&amp;quot;,
        &amp;quot;#ffb997&amp;quot;,
        &amp;quot;#263859&amp;quot;
    ],
    &amp;quot;cSpell.userWords&amp;quot;: [
        &amp;quot;YYYYMMDD&amp;quot;,
        &amp;quot;groupby&amp;quot;,
        &amp;quot;idxmax&amp;quot;,
        &amp;quot;inplace&amp;quot;,
        &amp;quot;itertools&amp;quot;,
        &amp;quot;multindex&amp;quot;,
        &amp;quot;rfind&amp;quot;,
        &amp;quot;strptime&amp;quot;
    ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;keyboard-shortcuts-json-file&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Keyboard Shortcuts JSON File&lt;/h3&gt;
&lt;p&gt;Updating the keyboard shortcuts JSON file is similar to editing &lt;code&gt;settings.json&lt;/code&gt;.
Open the Command Palette with &lt;code&gt;CMD+SHIFT+P&lt;/code&gt; and select &lt;code&gt;&#34;Preferences: Open Keyboard Shortcuts (JSON)&#34;&lt;/code&gt; to edit the JSON file where your settings are held.
You can copy/paste the entire block of JSON below or just individual lines.&lt;/p&gt;
&lt;p&gt;In addition to setting the keyboard shortcut &lt;code&gt;CMD+ENTER&lt;/code&gt; to execute lines in the
Interactive Window I have set a few other two keyboard shortcuts to work exactly
like RStudio. For example, clearing the console and restarting the R session:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CRTL+L&lt;/code&gt; - “Clear Console” (in RStudio) =&amp;gt; Clear Cells (in Python Interactive Window)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CMD+SHIFT+F10&lt;/code&gt; - “Restart R” (in RStudio) =&amp;gt; Restart Kernel (in Python Interactive Window)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Of course there are other shortcuts that you can configure like R’s block
comment command (&lt;code&gt;Code -&amp;gt; Comment/Uncomment Lines&lt;/code&gt;) (CMD+SHIFT+C). Simply set
the VS Code command &lt;code&gt;&#34;editor.action.commentLine&#34;&lt;/code&gt; to that shortcut.&lt;/p&gt;
&lt;p&gt;Another example is R’s command (&lt;code&gt;Code -&amp;gt; Reflow Comment&lt;/code&gt;) which hard wraps code
to 80 characters using &lt;code&gt;CMD+SHIFT+/&lt;/code&gt;. VS Code can do the same after installing
the extension called “rewrap” and then giving its command
&lt;code&gt;&#34;rewrap.rewrapComment&#34;&lt;/code&gt; the same keyboard shortcut. It is really up to you to
configure the shortcuts that you use most often.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;keybindings.json&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&#34;json&#34;&gt;&lt;code&gt;// Place your key bindings in this file to override the defaultsauto[]
[
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;python.datascience.execSelectionInteractive&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; python.datascience.featureenabled &amp;amp;&amp;amp; python.datascience.ownsSelection &amp;amp;&amp;amp; !findInputFocussed &amp;amp;&amp;amp; !notebookEditorFocused &amp;amp;&amp;amp; !replaceInputFocussed &amp;amp;&amp;amp; editorLangId == &amp;#39;python&amp;#39;&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;shift+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-python.datascience.execSelectionInteractive&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; python.datascience.featureenabled &amp;amp;&amp;amp; python.datascience.ownsSelection &amp;amp;&amp;amp; !findInputFocussed &amp;amp;&amp;amp; !notebookEditorFocused &amp;amp;&amp;amp; !replaceInputFocussed &amp;amp;&amp;amp; editorLangId == &amp;#39;python&amp;#39;&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;python.datascience.runcurrentcell&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; python.datascience.featureenabled &amp;amp;&amp;amp; python.datascience.hascodecells &amp;amp;&amp;amp; !editorHasSelection &amp;amp;&amp;amp; !notebookEditorFocused&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-python.datascience.runcurrentcell&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; python.datascience.featureenabled &amp;amp;&amp;amp; python.datascience.hascodecells &amp;amp;&amp;amp; !editorHasSelection &amp;amp;&amp;amp; !notebookEditorFocused&amp;quot;
    },    
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;notebook.cell.executeAndSelectBelow&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;notebookEditorFocused &amp;amp;&amp;amp; activeEditor == &amp;#39;workbench.editor.notebook&amp;#39;&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;shift+enter&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-notebook.cell.executeAndSelectBelow&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;notebookEditorFocused &amp;amp;&amp;amp; activeEditor == &amp;#39;workbench.editor.notebook&amp;#39;&amp;quot;
    },    
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+l&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;python.datascience.removeallcells&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;python.datascience.featureenabled &amp;amp;&amp;amp; !terminalFocus&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+l&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;workbench.action.terminal.clear&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;terminalFocus&amp;quot;
    },    
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+shift+f10&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;python.datascience.restartkernel&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+shift+c&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;editor.action.commentLine&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; !editorReadonly&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+/&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-editor.action.commentLine&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus &amp;amp;&amp;amp; !editorReadonly&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;cmd+i&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;python.datascience.showhistorypane&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+shift+/&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;rewrap.rewrapComment&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;alt+q&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-rewrap.rewrapComment&amp;quot;,
        &amp;quot;when&amp;quot;: &amp;quot;editorTextFocus&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;ctrl+shift+s&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;extension.updateSettings&amp;quot;
    },
    {
        &amp;quot;key&amp;quot;: &amp;quot;shift+alt+u&amp;quot;,
        &amp;quot;command&amp;quot;: &amp;quot;-extension.updateSettings&amp;quot;
    }
]&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;extensions-installation-script&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Extensions Installation Script&lt;/h3&gt;
&lt;p&gt;You can run each of the lines below in your Terminal or other Command Prompt
after installing VS Code in order to install the extensions. &lt;strong&gt;NOTE&lt;/strong&gt;: This does
require having the &lt;code&gt;code&lt;/code&gt; command in your PATH. On Mac use the following instructions
to install ‘code’ command in PATH: &lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line&#34;&gt;https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line&lt;/a&gt;.
Alternatively, you can search for extensions in the VS Code Marketplace and install
from there (more detail is available here:
&lt;a target=&#34;_blank&#34; href=&#34;https://code.visualstudio.com/docs/editor/extension-gallery#_browse-for-extensions&#34;&gt;https://code.visualstudio.com/docs/editor/extension-gallery#_browse-for-extensions&lt;/a&gt;).&lt;/p&gt;
&lt;pre class=&#34;sh&#34;&gt;&lt;code&gt;# editor visual aids
code --install-extension CoenraadS.bracket-pair-colorizer-2
code --install-extension oderwat.indent-rainbow
code --install-extension gerane.Theme-textmate

# development aids
code --install-extension eamodio.gitlens
code --install-extension aaron-bond.better-comments
code --install-extension Gruntfuggly.todo-tree
code --install-extension mikestead.dotenv
code --install-extension streetsidesoftware.code-spell-checker

# code formatting
code --install-extension ms-python.python
code --install-extension VisualStudioExptTeam.vscodeintellicode
code --install-extension TabNine.tabnine-vscode
code --install-extension christian-kohler.path-intellisense
code --install-extension stkb.rewrap
code --install-extension SonarSource.sonarlint-vscode

# file viewer tools
code --install-extension DotJoshJohnson.xml
code --install-extension jithurjacob.nbpreviewer
code --install-extension GrapeCity.gc-excelviewer

# remote development tools
code --install-extension ms-azuretools.vscode-docker
code --install-extension ms-vscode-remote.remote-containers
code --install-extension ms-vscode-remote.remote-ssh
code --install-extension ms-vscode-remote.remote-ssh-edit
code --install-extension ms-vscode-remote.remote-wsl
code --install-extension ms-vscode-remote.vscode-remote-extensionpack

# tool to save your settings as a gist
code --install-extension Shan.code-settings-sync&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>salesforcer 0.1.3 - Features for Better CRM Data Management</title>
      <link>https://stevenmortimer.com/salesforcer-0.1.3-features-for-better-crm-data-management/</link>
      <pubDate>Mon, 10 Jun 2019 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/salesforcer-0.1.3-features-for-better-crm-data-management/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/salesforcer-0.1.3---features-for-better-crm-data-management/salesforcer-logo.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The latest version of the &lt;strong&gt;salesforcer&lt;/strong&gt; package (v0.1.3) is now available on CRAN
and it is ready to help you better manage your Salesforce data. Along with a host
of bug fixes this release has three big features:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;Pull lists of deleted and updated records by timeframe and be able to undelete
them or permanently delete them from the Recycle Bin to free up space in your org&lt;/li&gt;
&lt;li&gt;Search for duplicates using the duplicate rules in your org and merge records&lt;/li&gt;
&lt;li&gt;Control API call behaviors to perform “All or None” inserts, bypass duplicate rules,
configure the transfer of related records when changing a record’s owner, and more&lt;/li&gt;
&lt;/ol&gt;
&lt;div id=&#34;pulling-lists-of-delete-and-updated-records&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Pulling Lists of Delete and Updated Records&lt;/h2&gt;
&lt;p&gt;Salesforce is an organizational tool and with many hands touching the system sometimes
you are called in to do some detective work to figure out which records have been
deleted or updated in your org. Luckily, Salesforce has your back. Their SOAP API has
two methods that will return the deleted or updated records within a timeframe. Those
two methods have been implemented in &lt;strong&gt;salesforcer&lt;/strong&gt; 0.1.3 as &lt;code&gt;sf_get_deleted()&lt;/code&gt; and
&lt;code&gt;sf_get_updated()&lt;/code&gt;. Here is an example pulling out anything deleted yesterday:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(tidyverse)
library(lubridate)

# if no timezone is provided UTC is assumed
deleted_recs &amp;lt;- sf_get_deleted(&amp;quot;Contact&amp;quot;, start = Sys.Date() - 1, end = Sys.Date())
# here is how to pull deleted using yesterday according to EDT with help from lubridate
deleted_recs &amp;lt;- sf_get_deleted(&amp;quot;Contact&amp;quot;, 
                               start = ymd(Sys.Date()-1, tz=&amp;quot;America/New_York&amp;quot;), 
                               end = ymd(Sys.Date(), tz=&amp;quot;America/New_York&amp;quot;))
deleted_recs
#&amp;gt; # A tibble: 125 x 2
#&amp;gt;   deletedDate         id                
#&amp;gt;   &amp;lt;dttm&amp;gt;              &amp;lt;chr&amp;gt;             
#&amp;gt; 1 2020-09-11 19:08:40 0033s000014AyIXAA0
#&amp;gt; 2 2020-09-11 19:08:40 0033s000014AyIbAAK
#&amp;gt; 3 2020-09-11 19:08:46 0033s000014AyIgAAK
#&amp;gt; 4 2020-09-11 19:11:08 0033s000014AyLFAA0
#&amp;gt; 5 2020-09-11 19:11:08 0033s000014AyLGAA0
#&amp;gt; # … with 120 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if you see records in there that you may want back, now it’s as simple as calling
the &lt;code&gt;sf_undelete()&lt;/code&gt; function with the ids of the records you want.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# undelete the first three records from the list of records deleted yesterday 
undeleted &amp;lt;- sf_undelete(deleted_recs$id[1:3])
undeleted
#&amp;gt; # A tibble: 3 x 2
#&amp;gt;   id                 success
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;  
#&amp;gt; 1 0033s000014AyIXAA0 TRUE   
#&amp;gt; 2 0033s000014AyIbAAK TRUE   
#&amp;gt; 3 0033s000014AyIgAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can double check that the records are actually no longer deleted by running a
query for them and checking the &lt;code&gt;IsDeleted&lt;/code&gt; field like this:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# check that the first three records are no longer deleted 
is_not_deleted &amp;lt;- sf_query(sprintf(&amp;quot;SELECT Id, IsDeleted 
                                    FROM Contact 
                                    WHERE Id IN (&amp;#39;%s&amp;#39;)&amp;quot;, 
                                   paste0(undeleted$id, collapse=&amp;quot;&amp;#39;,&amp;#39;&amp;quot;)))
is_not_deleted
#&amp;gt; # A tibble: 3 x 2
#&amp;gt;   Id                 IsDeleted
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;    
#&amp;gt; 1 0033s000014AyIXAA0 FALSE    
#&amp;gt; 2 0033s000014AyIbAAK FALSE    
#&amp;gt; 3 0033s000014AyIgAAK FALSE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let’s say you really do want them deleted and by that, I mean permanently
from your org so they do not count against your space quota. Now you can empty
items in your Recycle Bin. Here is an example where we delete the records we just
undeleted and then empty them from the Recycle Bin.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# delete the records to move them to the Recycle Bin, them empty them from it
deleted &amp;lt;- sf_delete(is_not_deleted$Id)
really_deleted &amp;lt;- sf_empty_recycle_bin(deleted$id)
really_deleted
#&amp;gt; # A tibble: 3 x 2
#&amp;gt;   id                 success
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;  
#&amp;gt; 1 0033s000014AyIXAA0 TRUE   
#&amp;gt; 2 0033s000014AyIbAAK TRUE   
#&amp;gt; 3 0033s000014AyIgAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you want to double-check the field values of a deleted record, just make sure to
set &lt;code&gt;queryall = TRUE&lt;/code&gt; in your query so that it returns deleted or archived records in the
resultset. Note that after records are deleted from the Recycle Bin using this call,
they can be queried using &lt;code&gt;queryall = TRUE&lt;/code&gt; for some time. Typically this time is
24 hours, but may be shorter or longer.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# permanently deleted records not there in regular query
not_visible &amp;lt;- sf_query(sprintf(&amp;quot;SELECT Id, IsDeleted 
                                 FROM Contact 
                                 WHERE Id IN (&amp;#39;%s&amp;#39;)&amp;quot;, 
                                paste0(really_deleted$id, collapse=&amp;quot;&amp;#39;,&amp;#39;&amp;quot;)))
not_visible
#&amp;gt; # A tibble: 0 x 0

# but still there if using queryall for ~24 hrs
still_visible &amp;lt;- sf_query(sprintf(&amp;quot;SELECT Id, IsDeleted 
                                   FROM Contact 
                                   WHERE Id IN (&amp;#39;%s&amp;#39;)&amp;quot;, 
                                  paste0(really_deleted$id, collapse=&amp;quot;&amp;#39;,&amp;#39;&amp;quot;)), 
                          queryall = TRUE)
still_visible
#&amp;gt; # A tibble: 3 x 2
#&amp;gt;   Id                 IsDeleted
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;    
#&amp;gt; 1 0033s000014AyIXAA0 TRUE     
#&amp;gt; 2 0033s000014AyIbAAK TRUE     
#&amp;gt; 3 0033s000014AyIgAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;searching-for-duplicates-and-merging-them&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Searching for Duplicates and Merging Them&lt;/h2&gt;
&lt;p&gt;Part of having a Salesforce instance means maintaining a high-level of data integrity.
That means not allowing duplicates to crop up and, if they do, merging and deleting
them quickly so that any activity with a record can be tracked in a single place.
In &lt;strong&gt;salesforcer&lt;/strong&gt; 0.1.3 you can find duplicates using the Duplicate Rules already in
place in your org. In the example below I will create three records with the same
email address. I’ve already set up a Duplicate Rule in my org to recognize and prevent
records from being creating if they have the same email address as another record already
in Salesforce. When I run &lt;code&gt;sf_find_duplicates()&lt;/code&gt; it finds all three records.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# create the records 
new_contacts &amp;lt;- tibble(FirstName = rep(&amp;quot;Rick&amp;quot;, 3), 
                       LastName = rep(&amp;quot;James&amp;quot;, 3), 
                       Email = rep(&amp;quot;rick.james@gmail.com&amp;quot;, 3),
                       Description = c(&amp;quot;Musician&amp;quot;, &amp;quot;Funk Legend&amp;quot;, NA))
new_contacts
#&amp;gt; # A tibble: 3 x 4
#&amp;gt;   FirstName LastName Email                Description
#&amp;gt;   &amp;lt;chr&amp;gt;     &amp;lt;chr&amp;gt;    &amp;lt;chr&amp;gt;                &amp;lt;chr&amp;gt;      
#&amp;gt; 1 Rick      James    rick.james@gmail.com Musician   
#&amp;gt; 2 Rick      James    rick.james@gmail.com Funk Legend
#&amp;gt; 3 Rick      James    rick.james@gmail.com &amp;lt;NA&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# insert the records
new_recs &amp;lt;- sf_create(new_contacts, &amp;quot;Contact&amp;quot;, 
                      DuplicateRuleHeader = list(allowSave = TRUE,
                                                 includeRecordDetails = FALSE,
                                                 runAsCurrentUser = TRUE))
new_recs
#&amp;gt; # A tibble: 3 x 2
#&amp;gt;   id                 success
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;  
#&amp;gt; 1 0033s000014B3IZAA0 TRUE   
#&amp;gt; 2 0033s000014B3IaAAK TRUE   
#&amp;gt; 3 0033s000014B3IbAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# find them as dupes
dupes_found &amp;lt;- sf_find_duplicates(search_criteria = list(Email = &amp;quot;rick.james@gmail.com&amp;quot;),
                                  object_name = &amp;quot;Contact&amp;quot;)
#&amp;gt; Using Contact rules:
#&amp;gt;   - Standard_Rule_for_Contacts_with_Duplicate_Leads
#&amp;gt;   - Contact_Dupe_Email
dupes_found
#&amp;gt; # A tibble: 6 x 2
#&amp;gt;   sObject Id                
#&amp;gt;   &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;             
#&amp;gt; 1 Contact 0033s000013Xt92AAC
#&amp;gt; 2 Contact 0033s000014B35QAAS
#&amp;gt; 3 Contact 0033s000014B35RAAS
#&amp;gt; 4 Contact 0033s000014B3IZAA0
#&amp;gt; 5 Contact 0033s000014B3IaAAK
#&amp;gt; # … with 1 more row&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recordset from &lt;code&gt;sf_find_duplicates()&lt;/code&gt; shows the object that the records were found in
and their Ids. You will notice that those are the same Ids as what we received back when
creating the records to verify that we found those same three records based on the duplicated
email address. You will also notice in the call that there is a message showing which
rules were executed when searching for duplicates, one of them being a custom matching rule
I created called &lt;code&gt;Contact_Dupe_Email&lt;/code&gt;. In order for this function to work you must
have Duplicate Rules configured in your system. There is a lot of good documentation
online about how to set up these rules. Here is a link to one Salesforce Trailhead article
that I like: &lt;a target=&#34;_blank&#34; href=&#34;https://trailhead.salesforce.com/en/content/learn/modules/sales_admin_duplicate_management/sales_admin_duplicate_management_unit_2&#34;&gt;Resolve and Prevent Duplicate Data&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now if I’d like to merge those three duplicates records into one I can do so using
the new &lt;code&gt;sf_merge()&lt;/code&gt; function. I need to pick one of the records as the &lt;code&gt;master record&lt;/code&gt;
and supply its Id to the function. The other records’ Ids will be the &lt;code&gt;victim_ids&lt;/code&gt;.
All of the fields on the master record will supercede the others, but if you would
like to override some of those fields, then just specify them in the &lt;code&gt;master_fields&lt;/code&gt;
argument. In this example, we are merging the second and third record into the first
record. However, I like the description “Funk Legend” from the second record to be
on the master record, so I just specify it in the &lt;code&gt;master_fields&lt;/code&gt; argument.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;merge_summary &amp;lt;- sf_merge(master_id = dupes_found$Id[1], 
                          victim_ids = dupes_found$Id[2:3], 
                          object_name = &amp;quot;Contact&amp;quot;,
                          master_fields = c(&amp;quot;Description&amp;quot; = &amp;quot;Funk Legend&amp;quot;))
merge_summary
#&amp;gt; # A tibble: 1 x 5
#&amp;gt;   id                 success mergedRecordIds updatedRelatedIds errors
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;   &amp;lt;list&amp;gt;          &amp;lt;lgl&amp;gt;             &amp;lt;lgl&amp;gt; 
#&amp;gt; 1 0033s000013Xt92AAC TRUE    &amp;lt;chr [2]&amp;gt;       NA                NA&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can check that the other two records that were merged into the master record have
now been deleted and that they are tagged in the &lt;code&gt;MasterRecordId&lt;/code&gt; field with the Id
of the master record they were merged into.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;merge_confirm &amp;lt;- sf_query(sprintf(&amp;quot;SELECT Id, MasterRecordId, IsDeleted 
                                   FROM Contact 
                                   WHERE Id IN (&amp;#39;%s&amp;#39;)&amp;quot;, 
                                  paste0(dupes_found$Id, collapse=&amp;quot;&amp;#39;,&amp;#39;&amp;quot;)), 
                          queryall = TRUE)
merge_confirm
#&amp;gt; # A tibble: 6 x 3
#&amp;gt;   Id                 IsDeleted MasterRecordId    
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;     &amp;lt;chr&amp;gt;             
#&amp;gt; 1 0033s000013Xt92AAC FALSE     &amp;lt;NA&amp;gt;              
#&amp;gt; 2 0033s000014B35QAAS TRUE      0033s000013Xt92AAC
#&amp;gt; 3 0033s000014B35RAAS TRUE      0033s000013Xt92AAC
#&amp;gt; 4 0033s000014B3IZAA0 FALSE     &amp;lt;NA&amp;gt;              
#&amp;gt; 5 0033s000014B3IaAAK FALSE     &amp;lt;NA&amp;gt;              
#&amp;gt; # … with 1 more row&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;controlling-api-call-behavior&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Controlling API Call Behavior&lt;/h2&gt;
&lt;p&gt;You may have noticed an argument called &lt;code&gt;all_or_none&lt;/code&gt; in the previous CRAN release
of &lt;strong&gt;salesforcer&lt;/strong&gt;. This argument is put into the header of the API call so that when
records are created/updated/deleted it will only complete if all records were successful,
otherwise all of the record changes are rolled back. This “All or None” behavior is just
one of over a dozen headers that the SOAP API accepts. The REST, Bulk, and Metadata APIs
also have their own headers which can be set. You can pass these controls into &lt;strong&gt;salesforcer&lt;/strong&gt;
functions directly or by setting the &lt;code&gt;control&lt;/code&gt; argument that now appears in many functions.
For example, if you are inserting records and would like to bypass the Duplicate Rules
in your org, then set the &lt;a target=&#34;_blank&#34; href=&#34;https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_header_duplicateruleheader.htm&#34;&gt;DuplicateRuleHeader&lt;/a&gt;) like this:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;new_contacts &amp;lt;- tibble(FirstName = rep(&amp;quot;Test&amp;quot;, 2),
                       LastName = rep(&amp;quot;Contact&amp;quot;, 2), 
                       Email = rep(&amp;quot;testcontact@gmail.com&amp;quot;, 2))

# add control to allow the creation of records that violate a duplicate email rule
new_recs &amp;lt;- sf_create(new_contacts, object_name = &amp;quot;Contact&amp;quot;, 
                      DuplicateRuleHeader = list(allowSave = TRUE,
                                                 includeRecordDetails = FALSE,
                                                 runAsCurrentUser = TRUE))
new_recs
#&amp;gt; # A tibble: 2 x 2
#&amp;gt;   id                 success
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;  
#&amp;gt; 1 0033s000014B3IjAAK TRUE   
#&amp;gt; 2 0033s000014B3IkAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the documentation you may notice that the control argument in the &lt;code&gt;sf_create()&lt;/code&gt; function definition
looks like this &lt;code&gt;control = list(...)&lt;/code&gt;. This allows you to include the control headers
as an argument right in the function or put them inside the &lt;code&gt;sf_control&lt;/code&gt; function.
An example of using the &lt;code&gt;sf_control()&lt;/code&gt; function looks like this:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;new_recs &amp;lt;- sf_create(new_contacts, object_name = &amp;quot;Contact&amp;quot;, 
                      control=sf_control(DuplicateRuleHeader=list(allowSave=TRUE,
                                                                  includeRecordDetails=FALSE,
                                                                  runAsCurrentUser=TRUE)))
new_recs
#&amp;gt; # A tibble: 2 x 2
#&amp;gt;   id                 success
#&amp;gt;   &amp;lt;chr&amp;gt;              &amp;lt;lgl&amp;gt;  
#&amp;gt; 1 0033s000014B3HhAAK TRUE   
#&amp;gt; 2 0033s000014B3HiAAK TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;sf_control()&lt;/code&gt; function works exactly like &lt;code&gt;glm.control()&lt;/code&gt; when using the &lt;code&gt;glm()&lt;/code&gt;
function in the &lt;strong&gt;stats&lt;/strong&gt; package. It will accept any of the extra arguments in the
function call and validate them to see if they are accepted by the API type and operation
that the function is performing. As mentioned before, there are over a dozen control
parameters that are now possible across the various APIs. Those are documented on the
pkgdown website’s documentation for &lt;a target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/reference/sf_control.html&#34;&gt;&lt;code&gt;sf_control()&lt;/code&gt;&lt;/a&gt;.
There is also a vignette which describes all of this in more detail at &lt;a target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/articles/passing-control-args.html&#34;&gt;Passing Control Args&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;links-to-github&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Links to GitHub&lt;/h2&gt;
&lt;p&gt;For a complete listing of all changes made in the 0.1.3 release of
&lt;strong&gt;salesforcer&lt;/strong&gt; please view the &lt;a target=&#34;_blank&#34; href=&#34;https://stevenmmortimer.github.io/salesforcer/news/index.html#salesforcer-0-1-3-release&#34;&gt;NEWS.md&lt;/a&gt;
file on the &lt;strong&gt;pkgdown&lt;/strong&gt; site. Bug reports and feature requests are welcome in the GitHub &lt;a target=&#34;_blank&#34;
href=&#34;https://github.com/StevenMMortimer/salesforcer/issues&#34;&gt;issues&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thank you for your continued support!&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>One R Package a Day</title>
      <link>https://stevenmortimer.com/one-r-package-a-day/</link>
      <pubDate>Wed, 27 Jun 2018 06:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/one-r-package-a-day/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/one-r-package-a-day/profile-pic.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Discover one R package a day by following the
&lt;a target=&#34;_blank&#34; href=&#34;https://twitter.com/RLangPackage&#34;&gt;&lt;span class=&#34;citation&#34;&gt;@RLangPackage&lt;/span&gt;&lt;/a&gt; account
on Twitter! Inspired by One R Tip Day
(&lt;a target=&#34;_blank&#34; href=&#34;https://twitter.com/RLangTip&#34;&gt;&lt;span class=&#34;citation&#34;&gt;@RLangTip&lt;/span&gt;&lt;/a&gt;) I created
a Twitter bot that will feature one package with its description and GitHub URL
each day. The R programming language (referred to as
&lt;a target=&#34;_blank&#34; href=&#34;https://twitter.com/hashtag/rstats&#34;&gt;#rstats&lt;/a&gt; on
Twitter) now has over 12,600 packages. Tweeting one each day would take more
than 34 years to get through them all and that doesn’t even consider the rapid
growth of new packages as shown in this
&lt;a target=&#34;_blank&#34; href=&#34;ttp://blog.revolutionanalytics.com/2017/01/cran-10000.html&#34;&gt;post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/one-r-package-a-day/r-package-growth.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The purpose of the &lt;a target=&#34;_blank&#34; href=&#34;https://twitter.com/RLangPackage&#34;&gt;&lt;span class=&#34;citation&#34;&gt;@RLangPackage&lt;/span&gt;&lt;/a&gt;
Twitter account is to feature the diversity of packages that R has to offer. Most
users have heard of &lt;strong&gt;ggplot2&lt;/strong&gt; and &lt;strong&gt;dplyr&lt;/strong&gt;, but there are thousands of other great
packages waiting to be discovered. In order to highlight those I took a list of all
packages on CRAN and filtered it down to ones with anywhere between 5,000 and
1,000,000 CRAN downloads. I then took a subset of that with anywhere between 10
and 1,000 stars on GitHub. I decided to focus on these mildly popular packages with
source code hosted on GitHub so that I can embed the link in the tweet and promote packages with
code that people have already started to explore. After following those subsetting rules,
one package is selected at random after narrowing to a list of packages that haven’t
already been tweeted. I am hosting the bot on Heroku and using Heroku Scheduler to
send out a tweet each day at 10:30am UTC or 6:30am Eastern Time. Below the credits
and resources is the sloppily written Python that’s currently being hosted on
Heroku and executed.&lt;/p&gt;
&lt;div id=&#34;credits&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Credits&lt;/h2&gt;
&lt;p&gt;I was found a couple different blog posts on creating a Twitter bot with R, namely
&lt;a target=&#34;_blank&#34; href=&#34;https://medium.com/@randerson112358/create-a-twitter-bot-using-r-5a94f1b1b886&#34;&gt;here&lt;/a&gt; and
&lt;a target=&#34;_blank&#34; href=&#34;https://rstudio-pubs-static.s3.amazonaws.com/54352_3afcd597884a4192b3361513f2d32699.html&#34;&gt;here&lt;/a&gt;,
but they didn’t involve deploying on Heroku or another cloud service. However, there were
plenty of Python based Twitter bot tutorials, so I took full advantage of them and went the Python route.
Below is the host of resources I considered while figuring out how to deploy the app, what Twitter
package to use, and some basic Python syntax which, embarrassingly, I should know by now.
I think most users will gravitate towards the &lt;strong&gt;tweepy&lt;/strong&gt; Python library but I found that
it throws up an error when using it with Conda 3.6. The issue is documented on GitHub
&lt;a target=&#34;_blank&#34; href=&#34;https://github.com/tweepy/tweepy/issues/894&#34;&gt;https://github.com/tweepy/tweepy/issues/894&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Used as a general guide to deploying: &lt;a href=&#34;http://briancaffey.github.io/2016/04/05/twitter-bot-tutorial.html&#34; class=&#34;uri&#34;&gt;http://briancaffey.github.io/2016/04/05/twitter-bot-tutorial.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Setting up credentials as config vars: &lt;a href=&#34;https://devcenter.heroku.com/articles/config-vars&#34; class=&#34;uri&#34;&gt;https://devcenter.heroku.com/articles/config-vars&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;How to load environment vars from file: &lt;a href=&#34;https://robinislam.me/blog/reading-environment-variables-in-python/&#34; class=&#34;uri&#34;&gt;https://robinislam.me/blog/reading-environment-variables-in-python/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Timeline dump code: &lt;a href=&#34;https://github.com/geduldig/TwitterAPI/blob/master/examples/dump_timeline.py&#34; class=&#34;uri&#34;&gt;https://github.com/geduldig/TwitterAPI/blob/master/examples/dump_timeline.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&#34;full-script&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Full Script&lt;/h2&gt;
&lt;pre class=&#34;python&#34;&gt;&lt;code&gt;# script.py
from os import environ
from os.path import join, dirname
from dotenv import load_dotenv
from re import sub
import pandas
from TwitterAPI import TwitterAPI, TwitterPager

# create .env file path
dotenv_path = join(dirname(__file__), &amp;#39;.env&amp;#39;)

# load file from the path
load_dotenv(dotenv_path)

if __name__ == &amp;quot;__main__&amp;quot;:

    # connect to api
    api = TwitterAPI(consumer_key=environ[&amp;#39;TWITTER_CONSUMER_KEY&amp;#39;],
                     consumer_secret=environ[&amp;#39;TWITTER_CONSUMER_SECRET&amp;#39;],
                     access_token_key=environ[&amp;#39;TWITTER_ACCESS_TOKEN&amp;#39;],
                     access_token_secret=environ[&amp;#39;TWITTER_ACCESS_TOKEN_SECRET&amp;#39;])

    # scrape all prior tweets to check which packages I&amp;#39;ve already tweeted about
    SCREEN_NAME = &amp;#39;RLangPackage&amp;#39;
    pager = TwitterPager(api,
                         &amp;#39;statuses/user_timeline&amp;#39;,
                         {&amp;#39;screen_name&amp;#39;: SCREEN_NAME, &amp;#39;count&amp;#39;: 100})

    # parse out the package name that occurs before the hyphen at the beginning
    previous_pks = []
    for item in pager.get_iterator(wait=3.5):
        if &amp;#39;text&amp;#39; in item:
            this_pkg = sub(&amp;quot;^(\w+) - (.*)&amp;quot;, &amp;quot;\\1&amp;quot;, item[&amp;#39;text&amp;#39;])
            previous_pks.append(this_pkg)

    # convert the package names to a dataframe
    prev_df = pandas.DataFrame({&amp;#39;name&amp;#39;: previous_pks})
    prev_df.set_index(&amp;#39;name&amp;#39;)

    # load the data I&amp;#39;ve compiled on R packages
    url = &amp;quot;https://raw.githubusercontent.com/StevenMMortimer/one-r-package-a-day/master/r-package-star-download-data.csv&amp;quot;
    all_df = pandas.read_csv(url)
    all_df.set_index(&amp;#39;name&amp;#39;)

    # do an &amp;quot;anti join&amp;quot; to throw away previously tweeted rows
    all_df = pandas.merge(all_df, prev_df, how=&amp;#39;outer&amp;#39;, indicator=True)
    all_df = all_df[all_df[&amp;#39;_merge&amp;#39;] == &amp;#39;left_only&amp;#39;]

    # focus on packages in middle ground of downloads and stars
    filtered_df = all_df[all_df[&amp;#39;stars&amp;#39;].notnull()]
    filtered_df = filtered_df[filtered_df[&amp;#39;stars&amp;#39;].between(10,1000)]
    filtered_df = filtered_df[filtered_df[&amp;#39;downloads&amp;#39;].notnull()]
    filtered_df = filtered_df[filtered_df[&amp;#39;downloads&amp;#39;].between(5000, 1000000)]

    # randomly select one of the remaining rows
    selected_pkg = filtered_df.sample(1)

    # pull out the name and description to see if we need to 
    # truncate because of Twitters 280 character limit
    prepped_name = selected_pkg.iloc[0][&amp;#39;name&amp;#39;]
    prepped_desc = sub(&amp;#39;\s+&amp;#39;, &amp;#39; &amp;#39;, selected_pkg.iloc[0][&amp;#39;description&amp;#39;])

    name_len = len(prepped_name)
    desc_len = len(prepped_desc)

    # 280 minus 3 for &amp;quot; - &amp;quot;, then minus 23 because links are counted as such,
    # then minus 9 for the &amp;quot; #rstats &amp;quot; hashtag
    if desc_len &amp;lt;= (280-3-23-9-name_len):
        prepped_desc = prepped_desc[0:desc_len]
    else:
        prepped_desc = prepped_desc[0:(280-6-23-9-name_len)] + &amp;quot;...&amp;quot;

    # cobble together the tweet text
    TWEET_TEXT = prepped_name + &amp;quot; - &amp;quot; + prepped_desc + &amp;quot; #rstats &amp;quot; + selected_pkg.iloc[0][&amp;#39;github_url&amp;#39;]
    print(TWEET_TEXT)

    # tweet it out to the world!
    r = api.request(&amp;#39;statuses/update&amp;#39;, {&amp;#39;status&amp;#39;: TWEET_TEXT})
    print(&amp;#39;SUCCESS&amp;#39; if r.status_code == 200 else &amp;#39;PROBLEM: &amp;#39; + r.text)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Scraping Responsibly with R</title>
      <link>https://stevenmortimer.com/scraping-responsibly-with-r/</link>
      <pubDate>Thu, 21 Jun 2018 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/scraping-responsibly-with-r/</guid>
      <description>
        


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/scraping-responsibly-with-r/always-scrape-responsibly.jpg&#34; /&gt;
I recently wrote a blog post &lt;a target=&#34;_blank&#34; href=&#34;https://stevenmortimer.com/most-starred-r-packages-on-github/&#34;&gt;here&lt;/a&gt;
comparing the number of CRAN downloads an R package gets relative to its number of
stars on GitHub. What I didn’t really think about during my analysis was whether or
not scraping CRAN was a violation of its Terms and Conditions. I simply copy and
pasted some code from &lt;a target=&#34;_blank&#34; href=&#34;https://www.r-bloggers.com/&#34;&gt;R-bloggers&lt;/a&gt;
that seemed to work and went on my merry way. In hindsight, it would have been better
to check whether or not the scraping was allowed and maybe find a better way to
get the information I needed. Of course, there was a much easier way to get the
CRAN package metadata using the function &lt;code&gt;tools::CRAN_package_db()&lt;/code&gt; thanks to a hint
from Maëlle Salmon provided in this tweet:&lt;/p&gt;
&lt;div style=&#34;margin: auto;&#34;&gt;
&lt;blockquote class=&#34;twitter-tweet&#34;&gt;
&lt;p lang=&#34;en&#34; dir=&#34;ltr&#34;&gt;
Cool work! I see you scraped the data, you could get a table with all pkgs and an URL column via tools::CRAN_package_db() which is gentler for CRAN website I think.&lt;br&gt;&lt;br&gt;Not sure their website has a robots.txt but in general &lt;a href=&#34;https://t.co/20TtlsGBOf&#34;&gt;https://t.co/20TtlsGBOf&lt;/a&gt; is useful when webscraping 🙂
&lt;/p&gt;
— Maëlle Salmon (&lt;span class=&#34;citation&#34;&gt;@ma_salmon&lt;/span&gt;) &lt;a href=&#34;https://twitter.com/ma_salmon/status/1009525735277678593?ref_src=twsrc%5Etfw&#34;&gt;June 20, 2018&lt;/a&gt;
&lt;/blockquote&gt;
&lt;script async src=&#34;https://platform.twitter.com/widgets.js&#34; charset=&#34;utf-8&#34;&gt;&lt;/script&gt;
&lt;/div&gt;
&lt;div id=&#34;how-to-check-if-scraping-is-permitted&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;How to Check if Scraping is Permitted&lt;/h2&gt;
&lt;p&gt;Also provided by Maëlle’s tweet was the recommendation for using the &lt;strong&gt;robotstxt&lt;/strong&gt; package
(currently having 27 Stars + one Star that I just added!). It doesn’t seem to be
well known as it only has 6,571 total downloads. I’m hoping this post will help spread
the word. It’s easy to use! In this case I’ll check whether or not CRAN permits bots
on specific resources of the domain.&lt;/p&gt;
&lt;p&gt;My other blog post analysis originally started with trying to get a list of all
current R packages on CRAN by parsing the HTML from &lt;a target=&#34;_blank&#34;
href=&#34;https://cran.rstudio.com/src/contrib&#34;&gt;https://cran.rstudio.com/src/contrib&lt;/a&gt;. The page looks like this:
&lt;img src=&#34;https://stevenmortimer.com/blog/scraping-responsibly-with-r/cran-screenshot.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The question is whether or not scraping this page is permitted according to the
&lt;code&gt;robots.txt&lt;/code&gt; file on the &lt;code&gt;cran.rstudio.com&lt;/code&gt; domain. This is where the
&lt;strong&gt;robotstxt&lt;/strong&gt; package can help us out. We can check simply by supplying the
domain and path that is used to form the full link we are interested in
scraping. If the &lt;code&gt;paths_allowed()&lt;/code&gt; function returns &lt;code&gt;TRUE&lt;/code&gt; then we should be
allowed to scrape, if it returns &lt;code&gt;FALSE&lt;/code&gt; then we are not permitted to scrape.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(robotstxt)

paths_allowed(
  paths  = &amp;quot;/src/contrib&amp;quot;, 
  domain = &amp;quot;cran.rstudio.com&amp;quot;, 
  bot    = &amp;quot;*&amp;quot;
)
#&amp;gt; [1] TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case the value that is returned is &lt;code&gt;TRUE&lt;/code&gt; meaning that bots are allowed to scrape that
particular path. This was how I originally scraped the list of current R packages, even though
you don’t really need to do that since there is the wonderful function &lt;code&gt;tools::CRAN_package_db()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After retrieving the list of packages I decided to scrape details from the DESCRIPTION
file of each package. Here is where things get interesting. CRAN’s &lt;a target=&#34;_blank&#34; 
href=&#34;http://cran.rstudio.com/robots.txt&#34;&gt;&lt;code&gt;robots.txt&lt;/code&gt;&lt;/a&gt; file shows that scraping the
DESCRIPTION file of each package is not allowed.
&lt;img src=&#34;https://stevenmortimer.com/blog/scraping-responsibly-with-r/robots-screenshot.png&#34; /&gt;
Furthermore, you can verify this using the &lt;strong&gt;robotstxt&lt;/strong&gt; package:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;paths_allowed(
  paths = &amp;quot;/web/packages/ggplot2/DESCRIPTION&amp;quot;,
  domain = &amp;quot;cran.r-project.org&amp;quot;, 
  bot    = &amp;quot;*&amp;quot;
)
#&amp;gt; [1] FALSE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, when I decided to scrape the package metadata I did it by parsing the HTML
from the canonical package link that resolves to the &lt;code&gt;index.html&lt;/code&gt; page for the package. For
example, &lt;a target=&#34;_blank&#34; href=&#34;https://cran.r-project.org/package=ggplot2&#34;&gt;https://cran.r-project.org/package=ggplot2&lt;/a&gt;
resolves to &lt;a target=&#34;_blank&#34; href=&#34;https://cran.r-project.org/web/packages/ggplot2/index.html&#34;&gt;https://cran.r-project.org/web/packages/ggplot2/index.html&lt;/a&gt;. If you check whether scraping is allowed on this page, the &lt;strong&gt;robotstxt&lt;/strong&gt; package
says that it is permitted.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;paths_allowed(
  paths = &amp;quot;/web/packages/ggplot2/index.html&amp;quot;,
  domain = &amp;quot;cran.r-project.org&amp;quot;, 
  bot    = &amp;quot;*&amp;quot;
)
#&amp;gt; [1] TRUE

paths_allowed(
  paths = &amp;quot;/web/packages/ggplot2&amp;quot;,
  domain = &amp;quot;cran.r-project.org&amp;quot;, 
  bot    = &amp;quot;*&amp;quot;
)
#&amp;gt; [1] TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a tricky situation because I can access the same information that is in the
DESCRIPTION file just by going to the &lt;code&gt;index.html&lt;/code&gt; page for the package where scraping
seems to be allowed. In the spirit of respecting CRAN it logically follows that I should
not be scraping the package index pages if the individual DESCRIPTION files are off-limits.
This is despite there being no formal instruction from the &lt;code&gt;robots.txt&lt;/code&gt; file about package
index pages. All in all, it was an interesting bit of work and glad that I was able
to learn about the &lt;strong&gt;robotstxt&lt;/strong&gt; package so I can have it in my toolkit going forward.&lt;/p&gt;
&lt;div style=&#34;text-align:center;font-size:1.2em;&#34;&gt;
&lt;b&gt;Remember to Always Scrape Responsibly!&lt;/b&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br&gt;
&lt;strong&gt;DISCLAIMER&lt;/strong&gt;: I only have a basic understanding of how &lt;code&gt;robots.txt&lt;/code&gt; files work
based on allowing or disallowing specified paths. I believe in this case CRAN’s
&lt;code&gt;robots.txt&lt;/code&gt; broadly permitted scraping, but too narrowly disallowed just the DESCRIPTION
files. Perhaps this goes back to an older time where those DESCRIPTION files really
were the best place for people to start scraping so it made sense to disallow them.
Or the reason could be something else entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt;: David G. Johnston messaged me and brought up an important point that
the &lt;code&gt;robots.txt&lt;/code&gt; file primarily is used to tell search engines whether or not
to crawl certain portions of a domain. The “Terms of Use” might be a better place
to begin looking for any limitations on the usage of the data for the type of project
you are creating.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Most Starred R Packages on GitHub</title>
      <link>https://stevenmortimer.com/most-starred-r-packages-on-github/</link>
      <pubDate>Mon, 18 Jun 2018 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/most-starred-r-packages-on-github/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/kePrint/kePrint.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/most-starred-r-packages-on-github/star-download-relationship.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;It seems like all the best R packages proudly use GitHub and have a README adorned
with badges across the top. The recent &lt;a target=&#34;_blank&#34; href=&#34;https://techcrunch.com/2018/06/04/microsoft-has-acquired-github-for-7-5b-in-microsoft-stock/&#34;&gt;Microsoft acquisition of GitHub&lt;/a&gt;
got me wondering: What proportion of current R packages use GitHub? Or at least refer
to it in the URL of the package description. Also, what is the relationship between
the number of CRAN downloads and the number of stars on a repository? My curiosity
got the best of me so I hastily wrote a script to pull the data.
Click &lt;a href=&#34;#full-script&#34;&gt;here&lt;/a&gt; to go straight to the full script and data included at the bottom
of this post. I acknowledge there are more elegant ways to have coded this, but let’s press on.&lt;/p&gt;
&lt;div id=&#34;pulling-list-of-packages-their-details&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Pulling List of Packages &amp;amp; their Details&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CORRECTION&lt;/strong&gt;: I orginally posted this article with code that scraped CRAN to get the
list of packages and their metadata. This could violate CRAN Terms and Conditions
and generally isn’t responsible. Thanks to Maëlle Salmon (&lt;a target=&#34;_blank&#34; href=&#34;https://twitter.com/ma_salmon&#34;&gt;&lt;span class=&#34;citation&#34;&gt;@ma_salmon&lt;/span&gt;&lt;/a&gt;)
I was reminded of how to scrape responsibly and that there is a function, &lt;code&gt;tools::CRAN_package_db()&lt;/code&gt;,
that returns all the metadata for the current packages on CRAN. I have written another
blog post entitled &lt;a target=&#34;_blank&#34; href=&#34;https://stevenmortimer.com/scraping-responsibly-with-r/&#34;&gt;Scraping Responsibly with R&lt;/a&gt;
that details how to use the &lt;strong&gt;robotstxt&lt;/strong&gt; package from rOpenSci to check a domain’s
directive on bots. Without scraping CRAN here is how you get the package metadata:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;pkgs &amp;lt;- tools::CRAN_package_db()
# remove duplicate MD5sum column since tibbles can&amp;#39;t handle duplicate column names
pkgs &amp;lt;- pkgs[,unique(names(pkgs))]
pkgs %&amp;gt;%
  select(Package, Version, Author, BugReports, URL) %&amp;gt;%
  as.tibble()
#&amp;gt; Warning: `as.tibble()` is deprecated as of tibble 2.0.0.
#&amp;gt; Please use `as_tibble()` instead.
#&amp;gt; The signature and semantics have changed, see `?as_tibble`.
#&amp;gt; This warning is displayed once every 8 hours.
#&amp;gt; Call `lifecycle::last_warnings()` to see where this warning was generated.
#&amp;gt; # A tibble: 16,070 x 5
#&amp;gt;    Package  Version Author                  BugReports          URL             
#&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;gt;  1 A3       1.0.0   &amp;quot;Scott Fortmann-Roe&amp;quot;    &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt;  2 aaSEA    1.1.0   &amp;quot;Raja Sekhara Reddy D.… &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt;  3 AATtools 0.0.1   &amp;quot;Sercan Kahveci [aut, … https://github.com… &amp;lt;NA&amp;gt;            
#&amp;gt;  4 ABACUS   1.0.0   &amp;quot;Mintu Nath [aut, cre]&amp;quot; &amp;lt;NA&amp;gt;                https://shiny.a…
#&amp;gt;  5 abbyyR   0.5.5   &amp;quot;Gaurav Sood [aut, cre… http://github.com/… http://github.c…
#&amp;gt;  6 abc      2.1     &amp;quot;Csillery Katalin [aut… &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt;  7 abc.data 1.0     &amp;quot;Csillery Katalin [aut… &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt;  8 ABC.RAP  0.9.0   &amp;quot;Abdulmonem Alsaleh [c… &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt;  9 abcADM   1.0     &amp;quot;Zongjun Liu [aut],\n … &amp;lt;NA&amp;gt;                &amp;lt;NA&amp;gt;            
#&amp;gt; 10 ABCanal… 1.2.1   &amp;quot;Michael Thrun, Jorn L… &amp;lt;NA&amp;gt;                https://www.uni…
#&amp;gt; # … with 16,060 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there I looked at the package description fields &lt;code&gt;&#34;URL&#34;&lt;/code&gt; and &lt;code&gt;&#34;BugReports&#34;&lt;/code&gt; to see
if either contained “github.com”. It turns out that 3,718 of the packages (29.3%
of the total) referenced GitHub. After retrieving the package metadata I pinged
the GitHub API to see if I could get the number of stars for the repository. Currently,
GitHub allows 5,000 authenticated requests per hour &lt;a target=&#34;_blank&#34; href=&#34;https://developer.github.com/v3/#rate-limiting&#34;&gt;(link)&lt;/a&gt;,
but out of all the packages only 3,718 referenced GitHub, so I could make all the requests at once.
Here is the function I used to take a cleaned up version of the package’s URL then
form a request to the GitHub API to get star counts:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# get the star count from a clean version of the package&amp;#39;s URL
gh_star_count &amp;lt;- function(url){
  stars  &amp;lt;- tryCatch({
    this_url &amp;lt;- gsub(&amp;quot;https://github.com/&amp;quot;, &amp;quot;https://api.github.com/repos/&amp;quot;, url)
    req &amp;lt;- GET(this_url, gtoken)
    stop_for_status(req)
    cont &amp;lt;- content(req)
    cont$stargazers_count  
  }, error = function(e){
    return(NA_integer_)
  })
  return(stars)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;analyzing-the-data&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Analyzing the Data&lt;/h2&gt;
&lt;p&gt;Once I had all the package detail data, I found that R packages, on average, have 35.7
GitHub stars, but the median number of stars is only 6! &lt;strong&gt;ggplot2&lt;/strong&gt; has the most
stars with 3,174. In my analysis I removed the &lt;strong&gt;xgboost&lt;/strong&gt;, &lt;strong&gt;h2o&lt;/strong&gt;, and &lt;strong&gt;feather&lt;/strong&gt; packages
which point to the repository of their implementations in many languages, not just R.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/most-starred-r-packages-on-github/star-distribution.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;What I really found interesting was comparing CRAN downloads to GitHub repo stars.
Using the &lt;strong&gt;cranlogs&lt;/strong&gt; package I was able to get the total package downloads dating
back to January 1, 2014. In contrast with the low star counts, the median downloads
for R packages is 8,975. Combining stars and downloads data I found that the median
R package has 903 downloads per star. Only 38.7% of packages had more than 10 stars,
which shows how hard stars are to get even if you’ve written a great package. I’m
not sure what proportion of R users frequently reference and contribute to GitHub,
but it would be interesting to compare that with the high ratios of downloads to stars.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/most-starred-r-packages-on-github/star-download-scatter.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;There are some real outliers in the data. For example, the &lt;strong&gt;Rcpp&lt;/strong&gt; package, perhaps the
most downloaded package of all-time, has 15.8M downloads and only 377 stars. Similarly, Hadley’s
&lt;strong&gt;scales&lt;/strong&gt; package has 9.4M downloads and only 115 stars. These support/helper packages just
don’t get the same star love as the headliners like &lt;strong&gt;ggplot2&lt;/strong&gt;, &lt;strong&gt;shiny&lt;/strong&gt;, and &lt;strong&gt;dplyr&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Of course, I could not help but check out the stats for some of the most prolific package authors.
After parsing out individuals with the &lt;code&gt;[&#34;aut&#34;, &#34;cre&#34;]&lt;/code&gt; roles I came to the not so surprising
conclusion that Hadley has the most stars of any author with 12,408 stars. In contrast, Dirk Eddelbuettel had one
of the lowest star-to-download ratios. For every ~38K downloads Dirk’s repositories will receive one star.
Pay no attention to the man behind the curtain since his &lt;strong&gt;Rcpp&lt;/strong&gt; package underpins a whole
host of packages without all the GitHub fanfare. Here is a list of popular R package
authors and their stats:&lt;/p&gt;
&lt;table class=&#34;table table-striped table-hover table-responsive&#34; style=&#34;margin-left: auto; margin-right: auto;&#34;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Author
&lt;/th&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Notable Packages
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Stars
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads Per Star
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
ggplot2, dplyr, httr
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
113,160,314
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
12,408
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
9,119.9
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Dirk Eddelbuettel
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Rcpp, BH
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
28,433,586
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
745
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
38,165.9
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Yihui Xie
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
knitr, rmarkdown, bookdown
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
42,472,860
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
6,315
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
6,725.7
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Winston Chang
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
R6, shiny
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
17,161,005
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,027
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,261.5
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Jennifer Bryan
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
readxl, gapminder, googlesheets
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
6,055,774
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,714
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,533.1
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
JJ Allaire
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
rstudioapi, reticulate, tensorflow
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
8,882,553
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,798
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,174.6
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Jeroen Ooms
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
jsonlite, curl, openssl
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
25,907,868
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,483
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
17,469.9
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Scott Chamberlain
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
geojsonio, taxize
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,770,664
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,528
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
700.4
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Jim Hester
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
devtools, memoise, readr
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
22,867,071
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,332
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
5,278.6
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Kirill Müller
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
tibble, DBI
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
36,159,009
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,077
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
33,573.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I’m sure you could create mixed models to determine the unique download to star relationship
for individuals. Also, you could use other package attributes to predict stars or downloads,
but I’ll leave that to another curious soul. I will include tables below regarding the
top 10 most downloaded, most starred, most and least downloaded per star.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;appendix&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Appendix&lt;/h2&gt;
&lt;div id=&#34;top-10-most-starred-packages&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Top 10 Most Starred Packages&lt;/h3&gt;
&lt;table class=&#34;table table-striped table-hover table-responsive&#34; style=&#34;margin-left: auto; margin-right: auto;&#34;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Name
&lt;/th&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Author
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Stars
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads Per Star
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
ggplot2
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
13,001,703
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,174
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,096.3
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
shiny
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Winston Chang
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,571,794
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,902
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,575.4
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
dplyr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
8,276,844
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,408
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,437.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
devtools
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Jim Hester
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
5,536,730
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,645
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,365.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
knitr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Yihui Xie
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
7,131,564
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,581
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,510.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
data.table
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Matt Dowle
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
6,005,795
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,457
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,122.0
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
plotly
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Carson Sievert
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,195,880
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,255
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
952.9
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
rmarkdown
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Yihui Xie
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
5,432,495
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,160
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,683.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
tensorflow
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
JJ Allaire
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
94,856
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,033
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
91.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
bookdown
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Yihui Xie
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
126,586
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,009
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
125.5
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div id=&#34;top-10-most-downloaded-packages-with-stars&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Top 10 Most Downloaded Packages with Stars&lt;/h3&gt;
&lt;table class=&#34;table table-striped table-hover table-responsive&#34; style=&#34;margin-left: auto; margin-right: auto;&#34;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Name
&lt;/th&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Author
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Stars
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads Per Star
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Rcpp
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Dirk Eddelbuettel
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
15,824,781
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
377
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
41,975.5
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
ggplot2
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
13,001,703
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,174
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4,096.3
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
stringr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
11,547,828
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
268
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
43,088.9
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
stringi
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Marek Gagolewski
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
11,310,113
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
122
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
92,705.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
digest
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Dirk Eddelbuettel with contributions
by Antoine Lucas
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
11,233,244
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
42
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
267,458.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
plyr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
10,340,396
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
470
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
22,000.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
R6
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Winston Chang
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
9,993,128
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
212
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
47,137.4
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
reshape2
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
9,582,245
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
173
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
55,388.7
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
scales
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
9,380,757
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
115
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
81,571.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
jsonlite
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Jeroen Ooms
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
9,112,790
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
176
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
51,777.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div id=&#34;top-10-packages-by-stars-per-download-frequently-starred&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Top 10 Packages by Stars per Download (frequently starred)&lt;/h3&gt;
&lt;table class=&#34;table table-striped table-hover table-responsive&#34; style=&#34;margin-left: auto; margin-right: auto;&#34;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Name
&lt;/th&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Author
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Stars
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads Per Star
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
r2d3
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Javier Luraschi
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
416
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
235
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1.77
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
workflowr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
John Blischak
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
448
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
169
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2.65
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
goodpractice
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hannah Frick
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
523
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
192
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2.72
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
xtensor
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Johan Mabille
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,057
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
664
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3.10
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
scico
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Thomas Lin Pedersen
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
185
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
59
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3.14
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
shinytest
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Winston Chang
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
418
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
113
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3.70
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
furrr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Davis Vaughan
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
724
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
171
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4.23
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
pkgdown
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,589
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
332
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
4.79
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
rtika
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Sasha Goodman
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
168
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
32
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
5.25
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
mindr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Peng Zhao
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,051
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
368
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
5.57
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div id=&#34;bottom-10-packages-by-stars-per-download-infrequently-starred&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Bottom 10 Packages by Stars per Download (infrequently starred)&lt;/h3&gt;
&lt;table class=&#34;table table-striped table-hover table-responsive&#34; style=&#34;margin-left: auto; margin-right: auto;&#34;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Name
&lt;/th&gt;
&lt;th style=&#34;text-align:left;&#34;&gt;
Author
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Stars
&lt;/th&gt;
&lt;th style=&#34;text-align:right;&#34;&gt;
Downloads Per Star
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
mime
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Yihui Xie
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
7,398,765
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
12
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
616,563.8
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
pkgmaker
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Renaud Gaujoux
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,228,173
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
614,086.5
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
rngtools
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Renaud Gaujoux
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1,224,959
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
612,479.5
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
magic
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Robin K. S. Hankin
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
344,741
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
1
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
344,741.0
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
gsubfn
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
G. Grothendieck
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
675,056
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
337,528.0
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
bindrcpp
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Kirill Müller
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,996,452
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
10
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
299,645.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
plogr
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Kirill Müller
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
3,343,099
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
12
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
278,591.6
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
digest
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Dirk Eddelbuettel with contributions
by Antoine Lucas
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
11,233,244
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
42
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
267,458.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
munsell
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Charlotte Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
7,778,712
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
31
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
250,926.2
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
proto
&lt;/td&gt;
&lt;td style=&#34;text-align:left;&#34;&gt;
Hadley Wickham
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
2,593,246
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
11
&lt;/td&gt;
&lt;td style=&#34;text-align:right;&#34;&gt;
235,749.6
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div id=&#34;full-script&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Full Script&lt;/h3&gt;
&lt;p&gt;Below and available via gist with data at: &lt;a target=&#34;_blank&#34; href=&#34;https://gist.github.com/StevenMMortimer/1b4b626d3d91240a77f969ae04b37114&#34;&gt;https://gist.github.com/StevenMMortimer/1b4b626d3d91240a77f969ae04b37114&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# load packages &amp;amp; custom functions ---------------------------------------------

library(tidyverse)
library(httr)
library(cranlogs)
library(ggrepel)
library(scales)
library(knitr)
library(stringr)

gh_from_url &amp;lt;- function(x){
  if(!grepl(&amp;#39;,&amp;#39;, x)){
    x &amp;lt;- strsplit(x, &amp;quot; &amp;quot;)[[1]]
    x &amp;lt;- trimws(x[min(which(grepl(pattern=&amp;#39;http://github.com|https://github.com|http://www.github.com&amp;#39;, x, ignore.case=TRUE)))])    
  } else {
    x &amp;lt;- strsplit(x, &amp;quot;,&amp;quot;)[[1]]
    x &amp;lt;- trimws(x[min(which(grepl(pattern=&amp;#39;http://github.com|https://github.com|http://www.github.com&amp;#39;, x, ignore.case=TRUE)))])    
  }
  x &amp;lt;- gsub(&amp;quot;http://&amp;quot;, &amp;quot;https://&amp;quot;, tolower(x))
  x &amp;lt;- gsub(&amp;quot;www\\.github\\.com&amp;quot;, &amp;quot;github.com&amp;quot;, x)
  x &amp;lt;- gsub(&amp;quot;/$&amp;quot;, &amp;quot;&amp;quot;, x)
  x &amp;lt;- gsub(&amp;quot;^github.com&amp;quot;, &amp;quot;https://github.com&amp;quot;, x)
  x &amp;lt;- gsub(&amp;quot;/issues&amp;quot;, &amp;quot;&amp;quot;, x)
  x &amp;lt;- gsub(&amp;quot;\\.git&amp;quot;, &amp;quot;&amp;quot;, x)
  return(x)
}

aut_maintainer_from_details &amp;lt;- function(x){
  x &amp;lt;- gsub(&amp;quot;&amp;#39;|\&amp;quot;&amp;quot;, &amp;quot;&amp;quot;, x)
  if(grepl(&amp;#39;,&amp;#39;, x)){
    x &amp;lt;- strsplit(x, &amp;quot;\\],&amp;quot;)[[1]]
    aut_cre_ind &amp;lt;- grepl(pattern=&amp;#39;\\[aut, cre|\\[cre, aut|\\[cre&amp;#39;, x, ignore.case=TRUE)
    if(any(aut_cre_ind)){
      x &amp;lt;- x[min(which(aut_cre_ind))]
      x &amp;lt;- gsub(&amp;quot;\\[aut, cre|\\[cre, aut|\\[cre&amp;quot;, &amp;quot;&amp;quot;, x)
    }
    x &amp;lt;- strsplit(x, &amp;quot;,&amp;quot;)[[1]][1]
    x &amp;lt;- trimws(gsub(&amp;quot;\\]&amp;quot;, &amp;quot;&amp;quot;, x))
    x &amp;lt;- trimws(gsub(&amp;quot; \\[aut&amp;quot;, &amp;quot;&amp;quot;, x))
  }
  return(x)
}

gh_star_count &amp;lt;- function(url){
  stars  &amp;lt;- tryCatch({
    this_url &amp;lt;- gsub(&amp;quot;https://github.com/&amp;quot;, &amp;quot;https://api.github.com/repos/&amp;quot;, url)
    req &amp;lt;- GET(this_url, gtoken)
    stop_for_status(req)
    cont &amp;lt;- content(req)
    cont$stargazers_count  
  }, error = function(e){
    return(NA_integer_)
  })
  return(stars)
}

# authenticate to github -------------------------------------------------------
# use Hadley&amp;#39;s key and secret
myapp &amp;lt;- oauth_app(&amp;quot;github&amp;quot;,
                   key = &amp;quot;56b637a5baffac62cad9&amp;quot;,
                   secret = &amp;quot;8e107541ae1791259e9987d544ca568633da2ebf&amp;quot;)
github_token &amp;lt;- oauth2.0_token(oauth_endpoints(&amp;quot;github&amp;quot;), myapp)
gtoken &amp;lt;- config(token = github_token)

# pull list of packages --------------------------------------------------------

# get list of currently available packages on CRAN
pkgs &amp;lt;- tools::CRAN_package_db()

# remove duplicate MD5sum column since tibbles can&amp;#39;t handle duplicate column names
pkgs &amp;lt;- pkgs[,unique(names(pkgs))]

# filter out lines any duplicates
pkgs &amp;lt;- pkgs %&amp;gt;% 
  rename(Name = Package) %&amp;gt;%
  distinct(Name, .keep_all = TRUE)

# get details for each package -------------------------------------------------

all_pkg_details &amp;lt;- NULL

# old fashioned looping! 
# WARNING: This takes awhile to complete
for(i in 1:nrow(pkgs)){
  
  if(i %% 100 == 0){
    message(sprintf(&amp;quot;Processing package #%s out of %s&amp;quot;, i, nrow(pkgs)))
  }
  
  this_url &amp;lt;- pkgs[i,]$URL
  on_github &amp;lt;- FALSE
  this_github_url &amp;lt;- NA_character_
  gh_stars &amp;lt;- NA_integer_
  if(!is.null(this_url)){
    on_github &amp;lt;- grepl(&amp;#39;http://github.com|https://github.com|http://www.github.com&amp;#39;, this_url)
    if(on_github){
      this_github_url &amp;lt;- gh_from_url(this_url)
      gh_stars &amp;lt;- gh_star_count(this_github_url)
    } else {
      # check the BugReports URL as a backup (e.g. shiny package references GitHub this way)
      issues_on_github &amp;lt;- grepl(&amp;#39;http://github.com|https://github.com|http://www.github.com&amp;#39;, pkgs[i,]$BugReports)
      if(length(issues_on_github) == 0 || !issues_on_github){
        this_github_url &amp;lt;- NA_character_
      } else {
        this_github_url &amp;lt;- gh_from_url(pkgs[i,]$BugReports)
        gh_stars &amp;lt;- gh_star_count(this_github_url)  
        on_github &amp;lt;- TRUE
      }
    }
  } else {
    this_url &amp;lt;- NA_character_
  }
  
  downloads &amp;lt;- cran_downloads(pkgs[i,]$Name, from = &amp;quot;2014-01-01&amp;quot;, to = &amp;quot;2018-06-15&amp;quot;)
  
  all_pkg_details &amp;lt;- rbind(all_pkg_details, 
                           tibble(name = pkgs[i,]$Name, 
                                  published = pkgs[i,]$Published,
                                  author = aut_maintainer_from_details(pkgs[i,]$Author),
                                  url = this_url,
                                  github_ind = on_github, 
                                  github_url = this_github_url,
                                  downloads = sum(downloads$count),
                                  stars = gh_stars
                           )
  )
}

# basic summary stats ----------------------------------------------------------

# remove observations where the GitHub URL refers to a repository that 
# is not specific to R and therefore might have an inflated star count
all_pkg_details_clean &amp;lt;- all_pkg_details %&amp;gt;% 
  filter(!(name %in% c(&amp;#39;xgboost&amp;#39;, &amp;#39;h2o&amp;#39;, &amp;#39;feather&amp;#39;))) %&amp;gt;% 
  mutate(downloads_per_star = downloads / stars, 
         downloads_per_star = ifelse(!is.finite(downloads_per_star), NA_real_, downloads_per_star))

# proportion of all packages listing github
sum(all_pkg_details$github_ind)
mean(all_pkg_details$github_ind)
# proportion of packages with stars
mean(!is.na(all_pkg_details$stars))

# typical number of stars per package
mean(all_pkg_details_clean$stars, na.rm=TRUE)
median(all_pkg_details_clean$stars, na.rm=TRUE)
max(all_pkg_details_clean$stars, na.rm=TRUE)

# typical number of downloads per package
mean(all_pkg_details_clean$downloads, na.rm=TRUE)
median(all_pkg_details_clean$downloads, na.rm=TRUE)

# percent of packages over 10 stars
mean(all_pkg_details_clean$stars &amp;gt; 10, na.rm=TRUE)

mean(all_pkg_details_clean$downloads_per_star, na.rm=TRUE)
median(all_pkg_details_clean$downloads_per_star, na.rm=TRUE)


# stars histogram --------------------------------------------------------------
ggplot(data=all_pkg_details_clean, mapping=aes(stars)) +
  geom_histogram(aes(fill=..count..), bins=60) +
  scale_x_continuous(trans = &amp;quot;log1p&amp;quot;, breaks=c(0,1,2,3,10,100,1000,3000)) + 
  labs(x = &amp;quot;Stars&amp;quot;, 
       y = &amp;quot;Count&amp;quot;, 
       fill = &amp;quot;Count&amp;quot;, 
       caption = &amp;quot;Source: api.github.com as of 6/16/18&amp;quot;) + 
  ggtitle(&amp;quot;Distribution of GitHub Stars on R Packages&amp;quot;) + 
  theme_bw() + 
  theme(panel.grid.minor = element_blank(), 
        plot.caption=element_text(hjust = 0))

# stars to downloads scatterplot -----------------------------------------------
plot_dat &amp;lt;- all_pkg_details_clean
idx_label &amp;lt;- which(with(plot_dat, downloads &amp;gt; 10000000 | stars &amp;gt; 1000))
plot_dat$name2 &amp;lt;- plot_dat$name
plot_dat$name &amp;lt;- &amp;quot;&amp;quot;
plot_dat$name[idx_label] &amp;lt;- plot_dat$name2[idx_label]

ggplot(data=plot_dat, aes(stars, downloads, label = name)) +
  geom_point(color = ifelse(plot_dat$name == &amp;quot;&amp;quot;, &amp;quot;grey50&amp;quot;, &amp;quot;red&amp;quot;)) +
  geom_text_repel(box.padding = .5) + 
  scale_y_continuous(labels = comma) + 
  scale_x_continuous(labels = comma) + 
  labs(x = &amp;quot;GitHub Stars&amp;quot;, 
       y = &amp;quot;CRAN Downloads&amp;quot;, 
       caption = &amp;quot;Sources:\napi.github.com as of 6/16/18\ncranlogs as of 1/1/14 - 6/15/18&amp;quot;) + 
  ggtitle(&amp;quot;Relationship Between CRAN Downloads and GitHub Stars&amp;quot;) + 
  theme_bw() + 
  theme(plot.caption=element_text(hjust = 0))

# author stats -----------------------------------------------------------------

# summary by author
authors_detail &amp;lt;- all_pkg_details_clean %&amp;gt;% 
  group_by(author) %&amp;gt;% 
  summarize(downloads = sum(downloads, na.rm=TRUE), 
            stars = sum(stars, na.rm=TRUE)) %&amp;gt;% 
  mutate(downloads_per_star = downloads / stars, 
         downloads_per_star = ifelse(!is.finite(downloads_per_star), NA_real_, downloads_per_star)) %&amp;gt;% 
  arrange(desc(downloads))

# popular authors
pop_authors &amp;lt;- tibble(author = c(&amp;#39;Hadley Wickham&amp;#39;, 
                                 &amp;#39;Dirk Eddelbuettel&amp;#39;, 
                                 &amp;#39;Yihui Xie&amp;#39;, 
                                 &amp;#39;Winston Chang&amp;#39;, 
                                 &amp;#39;Jennifer Bryan&amp;#39;, 
                                 &amp;#39;JJ Allaire&amp;#39;,
                                 &amp;#39;Jeroen Ooms&amp;#39;, 
                                 &amp;#39;Scott Chamberlain&amp;#39;, 
                                 &amp;#39;Jim Hester&amp;#39;, 
                                 &amp;#39;Kirill Müller&amp;#39;), 
                      notable_packages = c(&amp;#39;ggplot2, dplyr, httr&amp;#39;,
                                           &amp;#39;Rcpp, BH&amp;#39;, 
                                           &amp;#39;knitr, rmarkdown, bookdown&amp;#39;, 
                                           &amp;#39;R6, shiny&amp;#39;, 
                                           &amp;#39;readxl, gapminder, googlesheets&amp;#39;, 
                                           &amp;#39;rstudioapi, reticulate, tensorflow&amp;#39;, 
                                           &amp;#39;jsonlite, curl, openssl&amp;#39;, 
                                           &amp;#39;geojsonio, taxize&amp;#39;, 
                                           &amp;#39;devtools, memoise, readr&amp;#39;, 
                                           &amp;#39;tibble, DBI&amp;#39;)
)
author_stats &amp;lt;- pop_authors %&amp;gt;% 
  inner_join(., authors_detail, by=&amp;#39;author&amp;#39;) %&amp;gt;%
  select(author, notable_packages, downloads, stars, downloads_per_star) %&amp;gt;% 
  mutate(downloads_per_star = round(downloads_per_star, 1)) %&amp;gt;%
  rename_all(. %&amp;gt;% gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, .) %&amp;gt;% str_to_title)

# single author 
#all_pkg_details_clean %&amp;gt;% filter(author == &amp;#39;Dirk Eddelbuettel&amp;#39;) %&amp;gt;% arrange(desc(downloads))  

# top 10 lists -----------------------------------------------------------------

# Top 10 Most Starred Packages

top_starred &amp;lt;- all_pkg_details_clean %&amp;gt;% 
  select(name, author, downloads, stars, downloads_per_star) %&amp;gt;%
  arrange(desc(stars)) %&amp;gt;%
  slice(1:10) %&amp;gt;% 
  mutate(downloads_per_star = round(downloads_per_star, 1)) %&amp;gt;%
  rename_all(. %&amp;gt;% gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, .) %&amp;gt;% str_to_title)

# Top 10 Most Downloaded Packages with stars

top_downloaded &amp;lt;- all_pkg_details_clean %&amp;gt;% 
  filter(!is.na(stars)) %&amp;gt;%
  select(name, author, downloads, stars, downloads_per_star) %&amp;gt;%
  arrange(desc(downloads)) %&amp;gt;%
  slice(1:10) %&amp;gt;% 
  mutate(downloads_per_star = round(downloads_per_star, 1)) %&amp;gt;%
  rename_all(. %&amp;gt;% gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, .) %&amp;gt;% str_to_title)

# Bottom 10 Packages by Downloads per Star (frequently starred) 

frequently_starred &amp;lt;- all_pkg_details_clean %&amp;gt;% 
  filter(downloads &amp;gt; 100) %&amp;gt;% 
  select(name, author, downloads, stars, downloads_per_star) %&amp;gt;%
  arrange(downloads_per_star) %&amp;gt;%
  slice(1:10) %&amp;gt;% 
  mutate(downloads_per_star = round(downloads_per_star, 2)) %&amp;gt;%
  rename_all(. %&amp;gt;% gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, .) %&amp;gt;% str_to_title)

# Top 10 Packages by Downloads per Star (infrequently starred) 

infrequently_starred &amp;lt;- all_pkg_details_clean %&amp;gt;% 
  select(name, author, downloads, stars, downloads_per_star) %&amp;gt;%
  arrange(desc(downloads_per_star)) %&amp;gt;%
  slice(1:10) %&amp;gt;% 
  rename_all(. %&amp;gt;% gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, .) %&amp;gt;% str_to_title)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Analyzing Customer Data from Square</title>
      <link>https://stevenmortimer.com/analyzing-customer-data-from-square/</link>
      <pubDate>Fri, 18 May 2018 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/analyzing-customer-data-from-square/</guid>
      <description>
        

&lt;div id=&#34;TOC&#34;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#the-square-data-model&#34;&gt;The Square Data Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#authenticating&#34;&gt;Authenticating&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#pulling-transaction-data&#34;&gt;Pulling Transaction Data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#spend-by-customer-group&#34;&gt;Spend by Customer Group&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#issues-with-the-apis&#34;&gt;Issues with the APIs&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#cannot-request-specific-fields&#34;&gt;Cannot Request Specific Fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#cannot-update-customer-groups-programmatically&#34;&gt;Cannot Update Customer Groups Programmatically&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#customer-id-not-on-transactions&#34;&gt;Customer ID Not On Transactions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;div id=&#34;the-square-data-model&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;The Square Data Model&lt;/h2&gt;
&lt;p&gt;Whether you own your own business or consult for a business using Square to capture
payment data, Square can offer some amazing opportunities to gain insights by
leveraging their Connect v1 &amp;amp; v2 APIs. The Square data backend operates much like
a CRM system that holds information about &lt;em&gt;transactions&lt;/em&gt; that &lt;em&gt;customers&lt;/em&gt; make
when purchasing &lt;em&gt;items&lt;/em&gt; offered at a &lt;em&gt;location&lt;/em&gt;. Naturally, there are API endpoints
for each of the object types italicized above (Locations, Customers, Transactions, and Items)
and many more endpoints for administrative tasks (employees, roles, timecards, refunds, etc.).&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;authenticating&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Authenticating&lt;/h2&gt;
&lt;p&gt;While working with Square data I decided to develop an R package
&lt;a target=&#34;_blank&#34; href=&#34;https://github.com/StevenMMortimer/squareupr&#34;&gt;&lt;b&gt;squareupr&lt;/b&gt;&lt;/a&gt;
that makes it easier to retrieve Square data from R so that you can focus on
the analysis. After installing the &lt;strong&gt;squareupr&lt;/strong&gt; package you must authenticate by
supplying a personal access token (PAT) or using an OAuth 2.0 flow. You can find your
PAT by logging into the &lt;a target=&#34;_blank&#34; href=&#34;https://squareup.com&#34;&gt;Square Dashboard&lt;/a&gt;
-&amp;gt; Apps -&amp;gt; My Apps. Create an app or click “Manage App” if you’ve already created one and
there you should see your personal access token:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/analyzing-customer-data-from-square/finding-your-pat.png&#34; /&gt;&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# The squareupr package is not yet available on CRAN so you must install from GitHub
# install.packages(&amp;quot;devtools&amp;quot;)
devtools::install_github(&amp;quot;StevenMMortimer/squareupr&amp;quot;)

library(tidyverse)
library(squareupr)

# authenticate using your Personal Access Token (PAT)
sq_auth(personal_access_token = &amp;quot;sq-Th1s1sMyPers0nalAcessT0ken&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The package also offers OAuth 2.0 authentication. More information is available
&amp;lt;a target=&#34;_blank&#34; href-“&lt;a href=&#34;https://github.com/StevenMMortimer/squareupr#authenticate&#34; class=&#34;uri&#34;&gt;https://github.com/StevenMMortimer/squareupr#authenticate&lt;/a&gt;”&amp;gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;pulling-transaction-data&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Pulling Transaction Data&lt;/h2&gt;
&lt;p&gt;As mentioned above there are endpoints for every major type of data stored by Square.
The &lt;a target=&#34;_blank&#34; href=&#34;https://docs.connect.squareup.com/api/connect/v2&#34;&gt;API documentation&lt;/a&gt;
does a very good job at laying out how requests should be made to each of those endpoints. One
thing to note is that it is important to first pull down the location details for
your business because the location is often required when searching for things like
transactions and items.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;our_locations &amp;lt;- sq_list_locations()
our_locations$name &amp;lt;- &amp;quot;{HIDDEN}&amp;quot;
our_locations %&amp;gt;% select(id, name, address, timezone, 
                        capabilities, status, created_at)
#&amp;gt; # A tibble: 5 x 7
#&amp;gt;   id            name     address  timezone  capabilities status created_at
#&amp;gt;   &amp;lt;chr&amp;gt;         &amp;lt;chr&amp;gt;    &amp;lt;list&amp;gt;   &amp;lt;chr&amp;gt;     &amp;lt;list&amp;gt;       &amp;lt;chr&amp;gt;  &amp;lt;chr&amp;gt;     
#&amp;gt; 1 46FYN9N9RQS54 {HIDDEN} &amp;lt;list [… America/… &amp;lt;list [1]&amp;gt;   ACTIVE 2017-04-2…
#&amp;gt; 2 DRDCJ2X8E2PMV {HIDDEN} &amp;lt;list [… America/… &amp;lt;list [1]&amp;gt;   ACTIVE 2016-09-2…
#&amp;gt; 3 8T1TYXE840S00 {HIDDEN} &amp;lt;list [… America/… &amp;lt;list [1]&amp;gt;   ACTIVE 2016-09-2…
#&amp;gt; 4 1AWPRVVVFWGQF {HIDDEN} &amp;lt;list [… America/… &amp;lt;list [1]&amp;gt;   ACTIVE 2017-04-1…
#&amp;gt; 5 50X1GNAWEC8V0 {HIDDEN} &amp;lt;list [… America/… &amp;lt;list [1]&amp;gt;   ACTIVE 2017-03-0…&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that you have the location ids if you would like to pull all of the transactions
during a given timeframe, you would use the function &lt;code&gt;sq_list_transactions()&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# list all transactions for our 2nd location on May 11, 2018
# by default, if a date is provided with no time, then the time component is set to midnight
our_transactions &amp;lt;- sq_list_transactions(location = our_locations$id[2], 
                                         begin_time = as.Date(&amp;#39;2018-05-11&amp;#39;), 
                                         end_time = as.Date(&amp;#39;2018-05-12&amp;#39;))
our_transactions
#&amp;gt; # A tibble: 245 x 6
#&amp;gt;    id          location_id  created_at    tenders product client_id       
#&amp;gt;    &amp;lt;chr&amp;gt;       &amp;lt;chr&amp;gt;        &amp;lt;chr&amp;gt;         &amp;lt;list&amp;gt;  &amp;lt;chr&amp;gt;   &amp;lt;chr&amp;gt;           
#&amp;gt;  1 bUjFGVjBvN… DRDCJ2X8E2P… 2018-05-12T0… &amp;lt;list … REGIST… D5528FBA-E5DE-4…
#&amp;gt;  2 5PZP31N5Zs… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… A3A1FF51-325A-4…
#&amp;gt;  3 BTrGydD6he… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 2B3D32EB-8E58-4…
#&amp;gt;  4 XsqOAHl68z… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… C50AF3D7-BE32-4…
#&amp;gt;  5 vmLRzrwByS… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 52E40E1B-2333-4…
#&amp;gt;  6 pTbzQApZW7… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 962766FF-1436-4…
#&amp;gt;  7 lnE20zklpP… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… A02191CC-9AC9-4…
#&amp;gt;  8 DSumrqQW0L… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 1135FF4F-9B89-4…
#&amp;gt;  9 tPwFXetIwe… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 0D95E79D-B44C-4…
#&amp;gt; 10 bqUuFrzH71… DRDCJ2X8E2P… 2018-05-11T2… &amp;lt;list … REGIST… 48FD6A49-80A9-4…
#&amp;gt; # ... with 235 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At first glance there does not appear to be very much detail on the transaction record.
However, the &lt;code&gt;tender&lt;/code&gt; field represents a method of payment used in a Square transaction
so it contains information regarding the amount of money paid in total, in Square fees,
and tip. The &lt;code&gt;tender&lt;/code&gt; field even contains information regarding the &lt;code&gt;customer_id&lt;/code&gt; and
credit card information. In the following I will loop through the transactions in
April 2018 and determine the total spend for each customer in the dataset. Note:
The call to &lt;code&gt;sq_list_transactions&lt;/code&gt; may take a couple minutes to complete if you are pulling
thousands of transactions.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;april_transactions &amp;lt;- sq_list_transactions(location = our_locations$id[2], 
                                           begin_time = as.Date(&amp;#39;2018-04-01&amp;#39;), 
                                           end_time = as.Date(&amp;#39;2018-05-01&amp;#39;))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In order to extract the customer ID and money spent I create a function
that checks for the &lt;code&gt;tender&lt;/code&gt; object on the transaction and if it exists tries
to extract the data into a &lt;code&gt;tbl_df&lt;/code&gt;. When I supply this function as
&lt;code&gt;map_df(extract_cust_info_func) %&amp;gt;%&lt;/code&gt; I get the data from each transaction stacked
into a single &lt;code&gt;tbl_df&lt;/code&gt; that’s ready to analyze.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# create a function that will extract out just the customer id and money spent
extract_cust_info_func &amp;lt;- function(x){
  if(!is.null(x$tender)){
    tibble(customer_id = sq_null_to_na(x$tender[[1]]$customer_id),
           money_spent = sq_null_to_na(x$tender[[1]]$amount_money$amount))  
  } else {
    tibble(customer_id = NA_character_, 
           money_spent = NA_integer_)
  }
}

april_customer_spend &amp;lt;- april_transactions %&amp;gt;% 
  transpose() %&amp;gt;%
  # pull out just the information we want from each transaction
  map_df(extract_cust_info_func) %&amp;gt;%
  group_by(customer_id) %&amp;gt;%
  summarize(total_spend = sum(money_spent, na.rm=TRUE))

april_customer_spend
#&amp;gt; # A tibble: 208 x 2
#&amp;gt;    customer_id                total_spend
#&amp;gt;    &amp;lt;chr&amp;gt;                            &amp;lt;int&amp;gt;
#&amp;gt;  1 064HFDQG0N52AHDBSG00C1BAC8        1700
#&amp;gt;  2 07VNWH1V4S6W4W2EJ4AN7SEJNR           0
#&amp;gt;  3 08M453QNJ97BCT97SM09TN7QK4           0
#&amp;gt;  4 08XE43X8FS0MPX8P2W4N0DEQY0         350
#&amp;gt;  5 0CZ78CVRW12V7AZET8S3S82GGW         675
#&amp;gt;  6 0G1J81148H42GGMTMQKRWSJHGC           0
#&amp;gt;  7 0V1Y1BX23WYRK889ERVBE2T0KM         900
#&amp;gt;  8 13HHFBFZTD33RX0RSJNAZQKV5M           0
#&amp;gt;  9 1BFCHB9MK91GQ39HTD7QK6R7ZR           0
#&amp;gt; 10 1DR0AK5GKX57H9ER9SG1JF01P0        2900
#&amp;gt; # ... with 198 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The amounts in &lt;code&gt;april_customer_spend&lt;/code&gt; may seem large, but the Square APIs return
money as integers that represent whole cents. If you divide by 100, then you will
have the money amounts in dollars.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;spend-by-customer-group&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Spend by Customer Group&lt;/h2&gt;
&lt;p&gt;Square has this concept of “groups” that customers belong to. These groups can be
fashioned to do marketing campaigns complete with email blasts. In our analysis
let’s further determine which groups these customers belong to. The Square API has
an endpoint to retrieve one customer at a time; however, with large lists you
may get subjected to rate limiting. Rate limiting is errors on your requests because
too many are coming from the same application or access token. I would recommend
pulling down the entire list of customers with &lt;code&gt;sq_list_customers()&lt;/code&gt; and then
matching them up in R.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;cust_groups &amp;lt;- sq_list_customers() %&amp;gt;%
  select(id, groups) %&amp;gt;%
  sq_extract_cust_groups() %&amp;gt;%
  # filter to only the groups designated automatically by Square
  filter(grepl(&amp;quot;^CQ689YH4KCJMY&amp;quot;, groups.id))

cust_groups
#&amp;gt; # A tibble: 13,444 x 3
#&amp;gt;    id                         groups.id                 groups.name       
#&amp;gt;    &amp;lt;chr&amp;gt;                      &amp;lt;chr&amp;gt;                     &amp;lt;chr&amp;gt;             
#&amp;gt;  1 M1RBDFRK7S1Q1EP6EZFJFV3CBW CQ689YH4KCJMY.LOYALTY_ALL Loyalty Participa…
#&amp;gt;  2 58MK9F1HQ5447D1QZDX60NHTP4 CQ689YH4KCJMY.CHURN_RISK  Lapsed            
#&amp;gt;  3 58MK9F1HQ5447D1QZDX60NHTP4 CQ689YH4KCJMY.REACHABLE   Reachable         
#&amp;gt;  4 MBSJA4QV4WX6N2XV8WV9VJJTG8 CQ689YH4KCJMY.LOYALTY_ALL Loyalty Participa…
#&amp;gt;  5 MBSJA4QV4WX6N2XV8WV9VJJTG8 CQ689YH4KCJMY.REACHABLE   Reachable         
#&amp;gt;  6 ZCBZJ234217KTV812WX4DP2404 CQ689YH4KCJMY.REACHABLE   Reachable         
#&amp;gt;  7 FKEMR8KZCN3BH98RV78PKHKQ1R CQ689YH4KCJMY.LOYALTY_ALL Loyalty Participa…
#&amp;gt;  8 FKEMR8KZCN3BH98RV78PKHKQ1R CQ689YH4KCJMY.LOYAL       Regulars          
#&amp;gt;  9 78VMJPJNK959AHH0ZQPXDXEG3C CQ689YH4KCJMY.LOYALTY_ALL Loyalty Participa…
#&amp;gt; 10 QASM1G54VX0QN2S15YS6KHEFCC CQ689YH4KCJMY.LOYAL       Regulars          
#&amp;gt; # ... with 13,434 more rows&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we know the customer memberships, let’s join back with the transaction data
to determine the average total spend across the different membership groups.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;cust_groups %&amp;gt;% 
  # bring in the spend data
  inner_join(., april_customer_spend, by=c(&amp;#39;id&amp;#39;=&amp;#39;customer_id&amp;#39;)) %&amp;gt;%
  # group by the customer groups to find average spend per group
  group_by(groups.name) %&amp;gt;%
  summarize(avg_spend = mean(total_spend / 100, na.rm=TRUE))
#&amp;gt; # A tibble: 6 x 2
#&amp;gt;   groups.name              avg_spend
#&amp;gt;   &amp;lt;chr&amp;gt;                        &amp;lt;dbl&amp;gt;
#&amp;gt; 1 Cards on File                569. 
#&amp;gt; 2 Lapsed                        20.3
#&amp;gt; 3 Loyalty Participants          13.5
#&amp;gt; 4 Nearing a Loyalty Reward      15.3
#&amp;gt; 5 Reachable                     51.0
#&amp;gt; 6 Regulars                      27.4&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;issues-with-the-apis&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Issues with the APIs&lt;/h2&gt;
&lt;p&gt;What is great about this analysis is that we can use the Square APIs to quickly and
reliably pull down transaction data, match it to customer information and
see how certain customer groupings or campaigns are performing. However, I did find
some quirks and drawbacks that I wish the Square team would consider:&lt;/p&gt;
&lt;div id=&#34;cannot-request-specific-fields&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Cannot Request Specific Fields&lt;/h3&gt;
&lt;p&gt;First, it would be nice to implement, as part of the query parameters, the ability to
only return certain fields (e.g. the &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;groups&lt;/code&gt; fields from the Customer object).
This would help for two reasons: 1) It would improve the speed since only the required
data would be passed across and 2) For analysts who do not want to deal with personally
identifiable information (even in RAM) you would not be forced to pull information like
phone number, email, names, and even credit card information.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;cannot-update-customer-groups-programmatically&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Cannot Update Customer Groups Programmatically&lt;/h3&gt;
&lt;p&gt;Second, the API will &lt;strong&gt;not&lt;/strong&gt; allow you to update the customer groups programmatically.
This means that you are stuck using the dashboard to create and assign customers to the groups you
want to analyze. It is a drag when you want to create a reproducible research workflow
to assign customer groups but the API hinders your ability to do so.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;customer-id-not-on-transactions&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Customer ID Not On Transactions&lt;/h3&gt;
&lt;p&gt;Finally, it appears that the customer ID associated to a transaction is not reliably captured
on every transaction’s tender object. This means that you cannot get a complete
picture of all the transactions. I believe part of this is an issue with the
merchant capturing the customer information at the point of sale, but also partly
Square’s fault since they seem to be able to piece everything together in their
dashboard. However, this is not the case with the APIs. Overall, the Square APIs
are a rich data resource for helping run a business and they should only get better
with time as development progresses.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Tips for Making Professional Shiny Apps with R</title>
      <link>https://stevenmortimer.com/tips-for-making-professional-shiny-apps-with-r/</link>
      <pubDate>Tue, 09 Jan 2018 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/tips-for-making-professional-shiny-apps-with-r/</guid>
      <description>
        

&lt;div id=&#34;TOC&#34;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#add-a-progress-wheel&#34;&gt;Add a Progress Wheel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#create-a-www-folder&#34;&gt;Create a www Folder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#style-with-css&#34;&gt;Style with CSS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#use-a-global.r-file&#34;&gt;Use a global.R file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#encourage-data-downloading&#34;&gt;Encourage Data Downloading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;If you’ve built a Shiny app with R you’ll know how exciting it feels to get your
first app up and running. Shiny is an amazing tool for R users, but how do you
take it to the next level? This post with outline five easy ways to make Shiny apps
more professional.&lt;/p&gt;
&lt;div id=&#34;add-a-progress-wheel&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Add a Progress Wheel&lt;/h3&gt;
&lt;p&gt;I’ll start with the progress wheel because it is the easiest and most impactful
change to make. Oftentimes, Shiny apps involve calculations that take more than
one second to execute. It can be a frustrating experience for the user to interact
with an application and wait without knowing what is happening. Is the application
busy doing some calculation? Did the user select the correct button? Is the server
down? You have likely been in this situation yourself as a user of the web. There
are a few different ways to implement a progress indicator, but I have found the
simplest and most effective solution already in the &lt;a href=&#34;https://github.com/AnalytixWare/ShinySky&#34;&gt;&lt;code&gt;shinysky&lt;/code&gt; package&lt;/a&gt;.
The &lt;code&gt;busyIndicator()&lt;/code&gt; function allows you to specify a custom message and wait
time before the progress wheel appears.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;busyIndicator(text = &amp;quot;Calculation in progress ... &amp;quot;, wait = 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img style=&#34;width:300px;&#34; src=&#34;https://stevenmortimer.com/blog/tips-for-making-professional-shiny-apps-with-r/busy-indicator.png&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;create-a-www-folder&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Create a www Folder&lt;/h3&gt;
&lt;p&gt;Most web applications contain extra resources or libraries to enhance their
appearance and functionality. Shiny apps are no different. If you would like to
include images, styling, or even JavaScript, then all of these files should be
contained inside a single folder entitled &lt;code&gt;www&lt;/code&gt; that is located in the same directory
as the &lt;code&gt;ui.R&lt;/code&gt; and &lt;code&gt;server.R&lt;/code&gt; files. The &lt;code&gt;www&lt;/code&gt; folder is the main location for all
web resources in your application. If you would like to enforce a global standard
of styling, then create a &lt;code&gt;.css&lt;/code&gt; file containing the CSS stylesheet language for
formatting. If you would like to add images or GIFs to your Shiny app, then place
them in the www folder as well. Shiny supports most image file formats such as
&lt;code&gt;.jpg&lt;/code&gt;, &lt;code&gt;.png&lt;/code&gt;, and &lt;code&gt;.gif&lt;/code&gt;. If you would like to add JavaScript logic, then
create a &lt;code&gt;.js&lt;/code&gt; file containing any functions or variables that you would
like to execute. Again, place this file inside the &lt;code&gt;www&lt;/code&gt; folder. Note that you must
explicitly reference these files inside the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; tag of your app. The best way to
add to your app is by using &lt;code&gt;tags$head()&lt;/code&gt; and &lt;code&gt;tags$link()&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;tags$head(
  tags$link(rel = &amp;quot;stylesheet&amp;quot;, type = &amp;quot;text/css&amp;quot;, 
            href = &amp;quot;style.css&amp;quot;)
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can even give your app a bookmark icon using the link tag instead of the head tag!&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;tags$link(rel = &amp;quot;shortcut icon&amp;quot;, type = &amp;quot;image/x-icon&amp;quot;, 
          href = &amp;quot;my-shortcut-icon.ico&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;style-with-css&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Style with CSS&lt;/h3&gt;
&lt;p&gt;If you are deploying your Shiny apps in a professional setting, it is a best practice
to format and style your apps using rules defined by the CSS stylesheet language.
This will reduce the amount of time and effort to style new applications and provide a clean
and consistent experience so that users quickly become familiar with how elements
of your apps function based on a consistent appearance. Using CSS in Shiny apps
is no different from styling a traditional web page, so any resource teaching CSS
will help, such as, the W3Schools CSS Tutorial available at &lt;a href=&#34;http://www.w3schools.com/css/&#34; target=&#34;_blank&#34;&gt;http://www.w3schools.com/css/&lt;/a&gt;.
A Shiny CSS guide is also available at &lt;a href=&#34;http://shiny.rstudio.com/articles/css.html&#34; target=&#34;_blank&#34;&gt;http://shiny.rstudio.com/articles/css.html&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;use-a-global.r-file&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Use a global.R file&lt;/h3&gt;
&lt;p&gt;It is good practice when coding software to place commonly used pieces of logic
in a central location. You can put this concept into practice by creating a &lt;code&gt;global.R&lt;/code&gt;
file for your Shiny app. The &lt;code&gt;global.R&lt;/code&gt; file is not a requirement for a
Shiny app, but it will reduce redundant code and increase the app’s speed by re-using
logic and variables from a global scope among concurrent app users. The &lt;code&gt;global.R&lt;/code&gt;
file should be located in the same directory as the &lt;code&gt;ui.R&lt;/code&gt; and &lt;code&gt;server.R&lt;/code&gt; files.
Whenever a Shiny app is first used, the &lt;code&gt;global.R&lt;/code&gt; file makes its logic available
to the &lt;code&gt;ui.R&lt;/code&gt; and &lt;code&gt;server.R&lt;/code&gt; files. This speeds up your app because it is only
runs once (when the app is first launched). Typically, &lt;code&gt;global.R&lt;/code&gt; is a good place
for loading libraries, sourcing R functions or variables needed by your app, and
setting options like &lt;code&gt;stringsAsFactors = FALSE&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;encourage-data-downloading&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Encourage Data Downloading&lt;/h3&gt;
&lt;p&gt;A big part of Shiny’s value proposition in the enterprise is promoting data democracy and the
free sharing of information for use to further benefit the company. Shiny app users may
prefer other tools, such as Excel or Tableau or Python. Empower your users and allow
them the flexibility to intergrate into their preferred tool. The best way to facilitate that
is to allow downloads of the data. The best package by far for displaying data and allowing for
downloads is the &lt;code&gt;datatable&lt;/code&gt; package. The following code snippet creates a row for your
download button and then the datatable. I recommend pulling the button to the right of the
screen using &lt;code&gt;float: right;&lt;/code&gt; in your CSS so the button is naturally placed at the top
right of your table and ready for the user to click.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;fluidRow(
  column(12, downloadButton(&amp;quot;downloadDataFromTable&amp;quot;, &amp;quot;Download Table Data&amp;quot;))
)
fluidRow(
  column(12, DT::dataTableOutput(&amp;quot;campaign_table&amp;quot;, width = &amp;quot;100%&amp;quot;))
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/tips-for-making-professional-shiny-apps-with-r/datatable.png&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Building a Corporate R Package</title>
      <link>https://stevenmortimer.com/building-a-corporate-r-package/</link>
      <pubDate>Fri, 03 Nov 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/building-a-corporate-r-package/</guid>
      <description>
        

&lt;div id=&#34;TOC&#34;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#analytical-functions&#34;&gt;Analytical Functions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#company-constants&#34;&gt;Company Constants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#templatingformatting&#34;&gt;Templating/Formatting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#data-source-connections&#34;&gt;Data Source Connections&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#tutorials&#34;&gt;Tutorials&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;div id=&#34;analytical-functions&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Analytical Functions&lt;/h3&gt;
&lt;p&gt;Creating functions is a programming best practice. They are easier to monitor,
test, and teach to others. If your team performs routine calculations, establish
a standard by adding functions to the shared library and encourage everyone to
use those functions. If your company works a lot with customer phone
numbers, then write a function to standardize the formatting. See the example
below for how to create and document these functions.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; Format A Column of Phone Numbers
#&amp;#39; 
#&amp;#39; This function takes a dataframe and formats a column
#&amp;#39; to match a 10-digit phone number (XXX) XXX-XXXX
#&amp;#39;
#&amp;#39; @usage cleanPhoneNumbers(dat, phoneColumnName, returnExtColumn=FALSE)
#&amp;#39; @keywords cleaning transformation etl salesforce
#&amp;#39; @param dat a data.frame containing a column for formatting
#&amp;#39; @param phoneColumnName a character string defining the target data.frame column to
#&amp;#39; format as (XXX) XXX-XXXX
#&amp;#39; @param returnExtColumn a logical specifying whether to return digits seemingly formatted as
#&amp;#39; an extension to the main phone number
#&amp;#39; @return A \code{data.frame} with a clean phone number column in the format (XXX) XXX-XXXX
#&amp;#39; @examples
#&amp;#39; \dontrun{
#&amp;#39; my_data &amp;lt;- data.frame(Phone=c(&amp;#39;2349325339 x7779&amp;#39;, &amp;#39;8888-888-888&amp;#39;, 
#&amp;#39;                               &amp;#39;2343-234-2341&amp;#39;, &amp;#39;(832) 234-3494&amp;#39;))
#&amp;#39; my_data %&amp;gt;% cleanPhoneNumbers(&amp;#39;Phone&amp;#39;)
#&amp;#39; }
#&amp;#39; @export
cleanPhoneNumbers &amp;lt;- function(dat, phoneColumnName, returnExtColumn=FALSE){
  
  if(!(phoneColumnName %in% names(dat))){
    
    message(paste(phoneColumnName, &amp;#39;was not in the dataset, no cleaning performed.&amp;#39;))
    return(dat)
    
  } else {
    
    original_values &amp;lt;- dat[,phoneColumnName]
    
    ext_part &amp;lt;- gsub(&amp;#39;(.*)(extension|ext|ex|x)(.*)&amp;#39;, &amp;#39;\\3&amp;#39;, dat[,phoneColumnName])
    ext_part &amp;lt;- ifelse(ext_part == dat[,phoneColumnName], &amp;#39;&amp;#39;, ext_part)
    ext_part &amp;lt;- ifelse(((is.na(ext_part)) | 
                          (ext_part == &amp;#39;&amp;#39;)), &amp;#39;&amp;#39;, 
                       gsub(&amp;quot;[^\\d]+&amp;quot;, &amp;quot;&amp;quot;, ext_part, perl=TRUE))
    
    dat[,phoneColumnName] &amp;lt;- gsub(&amp;#39;(.*)(extension|ext|ex|x)(.*)&amp;#39;, &amp;#39;\\1&amp;#39;, 
                                  dat[,phoneColumnName])
    
    dat[,phoneColumnName] &amp;lt;- ifelse(((is.na(dat[,phoneColumnName])) | 
                                        (dat[,phoneColumnName] == &amp;#39;&amp;#39;)), &amp;#39;&amp;#39;, 
                                     gsub(&amp;quot;[^\\d]+&amp;quot;, &amp;quot;&amp;quot;, dat[,phoneColumnName], perl=TRUE))
    
    # convert 11 digit numbers starting with 1 to 10 because we assume US country code
    dat[,phoneColumnName] &amp;lt;- ifelse(((nchar(dat[,phoneColumnName]) == 11) &amp;amp; 
                                       (grepl(&amp;#39;^1&amp;#39;, dat[,phoneColumnName]))), 
                                     substring(dat[,phoneColumnName], 2), 
                                    dat[,phoneColumnName])
    
    # if not 10 digits, then set to &amp;#39;&amp;#39;
    dat[,phoneColumnName] &amp;lt;- ifelse((nchar(dat[,phoneColumnName]) == 10), 
                                    dat[,phoneColumnName], &amp;#39;&amp;#39;)
    
    # if weird then set to &amp;#39;&amp;#39;
    wierd_vals &amp;lt;- c(&amp;#39;0000000000&amp;#39;, &amp;#39;1111111111&amp;#39;, &amp;#39;1234567789&amp;#39;, &amp;#39;1231231234&amp;#39;, 
                    &amp;#39;1234567890&amp;#39;, &amp;#39;8888888888&amp;#39;, &amp;#39;9999999999&amp;#39;)
    
    dat[,phoneColumnName] &amp;lt;- ifelse((dat[,phoneColumnName] %in% wierd_vals), &amp;#39;&amp;#39;, 
                                    dat[,phoneColumnName])
    
    dat[,phoneColumnName] &amp;lt;- gsub(&amp;#39;([0-9]{3})([0-9]{3})([0-9]{4})&amp;#39;, &amp;#39;(\\1) \\2-\\3&amp;#39;, 
                                  dat[,phoneColumnName])
    
    if(returnExtColumn){
      dat[,&amp;#39;phone_ext_parsed_out&amp;#39;] &amp;lt;- ext_part
      
      dat[,&amp;#39;phone_ext_parsed_out&amp;#39;] &amp;lt;- ifelse(dat[,phoneColumnName] == &amp;#39;&amp;#39;, &amp;#39;&amp;#39;, 
                                             dat[,&amp;#39;phone_ext_parsed_out&amp;#39;])
      
      dat[,&amp;#39;phone_ext_parsed_out&amp;#39;] &amp;lt;- strtrim(dat[,&amp;#39;phone_ext_parsed_out&amp;#39;], 5)
      
      pos &amp;lt;- which(names(dat) == phoneColumnName)
      pos2 &amp;lt;- which(names(dat) == &amp;#39;phone_ext_parsed_out&amp;#39;)
      rest &amp;lt;- seq(pos+1, ncol(dat))
      rest &amp;lt;- rest[rest!=pos2]
      
      dat &amp;lt;- dat[,c(1:pos, pos2, rest)]
    }
    
    return(dat)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I strongly encourage using the
&lt;a href=&#34;https://google.github.io/styleguide/Rguide.xml&#34; target=&#34;_blank&#34;&gt;Google Style Guide&lt;/a&gt; or
&lt;a href=&#34;http://adv-r.had.co.nz/Style.html&#34; target=&#34;_blank&#34;&gt;Hadley’s Style Guide&lt;/a&gt;.
Between the two of these guides I believe the two most important rules are:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;Use snake case&lt;/li&gt;
&lt;li&gt;Name your functions with verbs and variables with nouns&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div id=&#34;company-constants&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Company Constants&lt;/h3&gt;
&lt;p&gt;Again, if you’re looking to enforce standards, it is a great practice to include
company-wide constants in your R package. Create an R file called &lt;code&gt;constants.R&lt;/code&gt; and
define these constants as follows:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; @export
default_price &amp;lt;- 9.99 # code that defines a default price constant value
lockBinding(&amp;quot;default_price&amp;quot;, environment())

#&amp;#39; @export
min_price &amp;lt;- 4.50 # also define a minimum price of item
lockBinding(&amp;quot;min_price&amp;quot;, environment())

#&amp;#39; @export
price_cutoffs &amp;lt;- c(0, 5, 10, 15, Inf) # define a vector to consistently bin data
lockBinding(&amp;quot;price_cutoffs&amp;quot;, environment())&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;templatingformatting&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Templating/Formatting&lt;/h3&gt;
&lt;p&gt;A little bit of formatting goes a long way if your team routinely presents figures
internally and/or externally. Consider adapting a ggplot2 theme to your company’s
colors, fonts, and other styling. Here are two links to get you started in making
custom theme:&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;&lt;p&gt;Roger Peng’s Mastering Software Development in R&lt;br /&gt;
&lt;a href=&#34;https://bookdown.org/rdpeng/RProgDA/building-a-new-theme.html&#34; class=&#34;uri&#34;&gt;https://bookdown.org/rdpeng/RProgDA/building-a-new-theme.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Joey Stanley’s Blog - Custom Themes in ggplot2&lt;br /&gt;
&lt;a href=&#34;http://joeystanley.com/blog/custom-themes-in-ggplot2&#34; class=&#34;uri&#34;&gt;http://joeystanley.com/blog/custom-themes-in-ggplot2&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Also, you might find that a majority of stakeholders in your company prefer receiving
data in Excel. You can export data from R to Excel with bolded, proper case column
names, format columns as percentages and dollar signs so that your stakeholders are
happy getting data from you. I highly recommend working with the
&lt;a href=&#34;https://github.com/awalker89/openxlsx&#34; target=&#34;_blank&#34;&gt;openxlsx package&lt;/a&gt;,
which does not require Java. Below is a snippet of code that will transform column
names, set column widths based on the data, and format them based on some key
words in the column names like formatting columns ending in “pct” as a percentage.
All of this before exporting to an Excel file so that your stakeholders are happy.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; Add Data and Format it in an Excel Workbook
#&amp;#39;
#&amp;#39; This function is a convenience function for standardizing one-off reports
#&amp;#39; in a certain Excel format that is pretty and well-organized for consumers
#&amp;#39;
#&amp;#39; @usage add_data_to_excel(wb, sheet, dat, startRow=1, startCol=1)
#&amp;#39; @concept writes data to Excel workbook in a pretty format
#&amp;#39; @keywords excel microsoft data export formatting
#&amp;#39; @seealso \code{\link[openxlsx]{createWorkbook}}
#&amp;#39; @param wb a Workbook object containing a worksheet. 
#&amp;#39; See \code{\link[openxlsx]{createWorkbook}}.
#&amp;#39; @param sheet the worksheet to write to. Can be the worksheet index or name.
#&amp;#39; @param dat data.frame to be written in the sheet
#&amp;#39; @param startRow a vector specifiying the starting column to write to
#&amp;#39; @param startCol a vector specifiying the starting row to write to
#&amp;#39; @return a logical indicating the success of adding the data to 
#&amp;#39; the workbook and formatting
#&amp;#39; @examples 
#&amp;#39; \dontrun{
#&amp;#39; my_wb &amp;lt;- createWorkbook()
#&amp;#39; addWorksheet(my_wb, &amp;quot;Iris&amp;quot;)
#&amp;#39; # returns invisble TRUE
#&amp;#39; add_data_to_excel(my_wb, &amp;quot;Iris&amp;quot;, iris)
#&amp;#39; saveWorkbook(my_wb, file=&amp;#39;~/iris.xlsx&amp;#39;, overwrite = TRUE)
#&amp;#39; }
#&amp;#39; @export
add_data_to_excel &amp;lt;- function(wb, sheet, dat, startCol=1, startRow=1){
  
  # convert column names to more human readable format (with spaces and proper case)
  names(dat) &amp;lt;- gsub(&amp;quot;_&amp;quot;, &amp;quot; &amp;quot;, tolower(names(dat)))
  names(dat) &amp;lt;- str_to_title(names(dat))
  
  writeData(wb=wb, sheet=sheet, x=dat, startCol=startCol, startRow=startRow, 
            headerStyle=createStyle(textDecoration=&amp;#39;bold&amp;#39;, 
                                    wrapText=TRUE, 
                                    halign=&amp;#39;center&amp;#39;, 
                                    valign=&amp;#39;center&amp;#39;))
  for(i in 1:ncol(dat)){
    
    setColWidths(wb, 
                 sheet, 
                 cols = i + (startCol - 1), 
                 widths = pmax(12, 
                               pmin(
                                 max(nchar(sapply(dat[,i], 
                                                  as.character)), na.rm=TRUE), 
                                 40)))
    
    if(grepl(&amp;#39;Percent|Pct|%&amp;#39;, names(dat)[i])){
      
      addStyle(wb,
               sheet, 
               style = createStyle(numFmt=&amp;#39;0.0%&amp;#39;), 
               rows = (startRow + 1):(nrow(dat) + 100), 
               cols = i + (startCol - 1), 
               gridExpand = TRUE)
      
    } else if(grepl(&amp;#39;Dollars&amp;#39;, names(dat)[i])){
      
      addStyle(wb,
               sheet, 
               style = createStyle(numFmt=&amp;#39;$#,##0&amp;#39;),
               rows = (startRow + 1):(nrow(dat) + 100), 
               cols = i + (startCol - 1), 
               gridExpand = TRUE)
      
    } else {
      # dont add any formatting
    }
  }
  invisible(return(TRUE))
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;data-source-connections&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Data Source Connections&lt;/h3&gt;
&lt;p&gt;On the job you’re probably accessing the same data sources over and over. Help out
yourself and fellow teammates by standardizing how to connect and pull data. This
could be database connections, API connections, or other server connections. Below
are two examples, one for connecting to MS SQL Server database and one for connecting
to the Salesforce API.&lt;/p&gt;
&lt;div id=&#34;db-connection&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;DB Connection&lt;/h4&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; Establish a connection to Our DB and execute a query
#&amp;#39; 
#&amp;#39; This function connects to the DBMS using the shared_user, executes query, 
#&amp;#39; closes the connection, and returns the recordset if possible.
#&amp;#39;
#&amp;#39; @usage queryOurDB(query, db_environment=c(&amp;#39;prod&amp;#39;,&amp;#39;dev&amp;#39;), username=NULL, password=NULL)
#&amp;#39; @importFrom RODBC odbcDriverConnect odbcConnect sqlQuery odbcClose
#&amp;#39; @param query a character string defining a query to be executed against the DB
#&amp;#39; @param db_environment a character string either &amp;#39;prod&amp;#39; or &amp;#39;dev&amp;#39; upon 
#&amp;#39; which environment to execute against
#&amp;#39; @param username a character string defining your database username credential
#&amp;#39; @param password a character string defining your database password credential
#&amp;#39; @return A \code{data.frame} object of the query&amp;#39;s resultset
#&amp;#39; @seealso
#&amp;#39;  \code{\link[RODBC]{odbcConnect}},
#&amp;#39;  \code{\link[DBI]{sqlQuery}}
#&amp;#39; @examples
#&amp;#39; \dontrun{
#&amp;#39; # This query pulls the top 10 records from CustomerTable
#&amp;#39; my_query &amp;lt;- &amp;quot;SELECT TOP 10 * FROM DB.dbo.CustomerTable&amp;quot;
#&amp;#39; query_result &amp;lt;- queryOurDB(my_query)
#&amp;#39; }
#&amp;#39; @export
queryOurDB &amp;lt;- function(query, db_environment=c(&amp;#39;prod&amp;#39;, &amp;#39;dev&amp;#39;), 
                      username=NULL, password=NULL){
  
  # try to prevent anything really bad resulting from dropping tables by accident
  stopifnot(!grepl(&amp;quot;drop table&amp;quot;, query, ignore.case=TRUE))
  
  # set the username and password
  if(is.null(username) | is.null(password)){
    username &amp;lt;- &amp;quot;shared_user&amp;quot;
    password &amp;lt;- &amp;quot;shared_password&amp;quot;
  }
  
  # determine which database to query
  which_db &amp;lt;- match.arg(db_environment)
  if(which_db==&amp;#39;prod&amp;#39;){
    host &amp;lt;- &amp;quot;10.245.96.30&amp;quot; #Production DB
  } else {
    host &amp;lt;- &amp;quot;10.245.96.40&amp;quot; #Development DB 
  }
  
  # run query
  query_result &amp;lt;- tryCatch({
    con &amp;lt;- odbcDriverConnect(
      sprintf(&amp;#39;driver={SQL Server};server=%s,1433;database=DUMMY;uid=%s;pwd=%s&amp;#39;,
              host, username, password)
      )
    
    message(&amp;quot;running query&amp;quot;)
    sqlQuery(con, query)
  }, error = function(e){
    message(&amp;quot;There was an error with your query. See details below.&amp;quot;)
    message(e)
  }, finally = {
    tryCatch({
      if(exists(&amp;quot;con&amp;quot;)){
        odbcClose(con)
        message(&amp;quot;closed database connection&amp;quot;)
      }
    })
  })
  
  return(query_result)
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;api-connection&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;API Connection&lt;/h4&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; Establish a session with the Salesforce API
#&amp;#39; 
#&amp;#39; This function connects to a Salesforce instance via the API
#&amp;#39;
#&amp;#39; @usage createSFAPISession(environment=c(&amp;#39;prod&amp;#39;, &amp;#39;qa&amp;#39;), 
#&amp;#39;                           user=NULL, password=NULL, apiVersion=&amp;quot;38.0&amp;quot;)
#&amp;#39; @importFrom RForcecom rforcecom.login
#&amp;#39; @param username a character string defining the Salesforce API username credential
#&amp;#39; @param password a character string defining the Salesforce API password credential.
#&amp;#39; Note: that this should be the concatenation of your password and security token. See the
#&amp;#39; instructions in \code{\link[RForcecom]{rforcecom.login}} for an example format.
#&amp;#39; @param apiVersion (optional) Version of the REST API and SOAP API that you want to use. 
#&amp;#39; (e.g. &amp;quot;38.0&amp;quot;). Supported versions from v20.0 and up.
#&amp;#39; @return A session object as returned by \code{\link[RForcecom]{rforcecom.login}}
#&amp;#39; that can be used for subsequent calls to the Salesforce API.
#&amp;#39; @seealso
#&amp;#39;  \code{\link[RForcecom]{rforcecom.login}},
#&amp;#39; @examples
#&amp;#39; \dontrun{
#&amp;#39; # establish a connection
#&amp;#39; sf_session &amp;lt;- createSFAPISession()
#&amp;#39; 
#&amp;#39; # logout of that session
#&amp;#39; rforcecom.logout(sf_session)
#&amp;#39; }
#&amp;#39; @export
createSFAPISession &amp;lt;- function(environment=c(&amp;#39;prod&amp;#39;, &amp;#39;qa&amp;#39;), 
                              user=NULL, password=NULL, apiVersion=&amp;quot;38.0&amp;quot;){
  
  which_env &amp;lt;- match.arg(environment)
 
  if(is.null(user) | is.null(password)){
    if(which_env == &amp;#39;qa&amp;#39;){
      username &amp;lt;- &amp;quot;shared_user@gmail.com&amp;quot;
      password &amp;lt;- &amp;quot;shared_password&amp;quot;
      url &amp;lt;- &amp;#39;https://test.salesforce.com/&amp;#39;
    } else if(which_env == &amp;#39;prod&amp;#39;){
      username &amp;lt;- &amp;quot;shared_user@gmail.com&amp;quot;
      password &amp;lt;- &amp;quot;shared_password&amp;quot;
      url &amp;lt;- &amp;#39;https://login.salesforce.com/&amp;#39;
    } else {
      stop(paste(&amp;quot;Cannot find the specified environment:&amp;quot;, which_env))
    }
  }

  sf_session &amp;lt;- rforcecom.login(username=username, password=password, 
                                loginURL=url, apiVersion=apiVersion)
  return(sf_session)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: If you do not feel comfortable centralizing credentials for security reasons or
your team does not have shared credentials, then have those stored locally in a
profile and called by the package in a standardized way.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;tutorials&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Tutorials&lt;/h3&gt;
&lt;p&gt;Finally, but maybe most importantly, R packages help teammembers grow and on-board faster.
You can include sample datasets and plenty of examples within the R package so that
new members of your team don’t have to ask around how to do certain things. They can
read the documentation and try the examples for themselves. Consider the R package a legacy
asset of your team that will stand the test of time well after you’ve left the company.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Automating R Scripts with Cron</title>
      <link>https://stevenmortimer.com/automating-r-scripts-with-cron/</link>
      <pubDate>Tue, 15 Aug 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/automating-r-scripts-with-cron/</guid>
      <description>
        
&lt;script src=&#34;https://stevenmortimer.com/rmarkdown-libs/header-attrs/header-attrs.js&#34;&gt;&lt;/script&gt;


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/automating-r-scripts-with-cron/cronjob.png&#34; style=&#34;display:block;margin:auto;max-height:125px;&#34; /&gt;&lt;/p&gt;
&lt;div id=&#34;scheduling-a-script-to-execute&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Scheduling a Script to Execute&lt;/h2&gt;
&lt;p&gt;If you would like to automate R scripts, one method is to use the cron daemon
already packaged on Linux servers. Let’s say you have script in your home
directory called &lt;code&gt;random.R&lt;/code&gt; and you would like to schedule it to run at 5:30pm
every day. On the server you will need to edit the crontab and add the following
line:&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;30 17 * * * Rscript /home/my-user/random.R&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: You can add this line to the the crontab by typing&lt;/p&gt;
&lt;ol style=&#34;list-style-type: decimal&#34;&gt;
&lt;li&gt;&lt;code&gt;crontab -e&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;i&lt;/code&gt; to enter insert mode&lt;/li&gt;
&lt;li&gt;Paste your new entry in the file&lt;/li&gt;
&lt;li&gt;Press CTRL+C to exit insert mode&lt;/li&gt;
&lt;li&gt;Save and quit by typing &lt;code&gt;:wq&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For reference, it should look like this while adding:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/automating-r-scripts-with-cron/crontab.png&#34; style=&#34;display:block;margin:auto;max-height:100px;&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;sending-cron-results-to-your-email-inbox&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Sending Cron Results to your Email Inbox&lt;/h2&gt;
&lt;p&gt;It is exciting to have a routine part of your work automated, but you probably
want to monitor your script and the results its creating. Cron has a built-in
feature that can send an email whenever an scheduled script errors out. Just add
your email address to the &lt;code&gt;MAILTO&lt;/code&gt; option. If you want to include multiple emails,
then separate them with a semicolon, like so:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/automating-r-scripts-with-cron/crontab-w-email.png&#34; style=&#34;display:block;margin:auto;max-height:100px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: New servers may not be setup as an email server. You will not receive
any emails or even errors that it didn’t work if you set the &lt;code&gt;MAILTO&lt;/code&gt; parameter
but haven’t configured the server to send email. I recommend using Postfix, a
free and open-source mail transfer agent, which is fairly easy to setup. Below
are abbreviated instructions for an Ubuntu server. See the additional resources
or run a Google search if you need more detail on setting up a custom hostname,
domain, SASL authentication, etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;From the command prompt of the server use the following steps&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 1 - Update package information: &lt;code&gt;sudo apt-get update&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Step 2 - Insall Postfix: &lt;code&gt;sudo apt-get install postfix&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Step 3 - Complete two prompts to install Postfix:
&lt;ul&gt;
&lt;li&gt;Step 3a - Select &lt;code&gt;Internet Site&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Step 3b - Click &lt;code&gt;OK&lt;/code&gt; to approve the “System mail name” (usually the server’s assigned hostname, like &lt;code&gt;ip-172-88-99-0.ec2.internal&lt;/code&gt; for an AWS EC2 instance)&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Additional resources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://tecadmin.net/install-postfix-on-ubuntu/&#34;&gt;https://tecadmin.net/install-postfix-on-ubuntu/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;http://www.postfix.org/documentation.html&#34;&gt;http://www.postfix.org/documentation.html&lt;/a&gt;&lt;br /&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a target=&#34;_blank&#34; href=&#34;https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-on-ubuntu-16-04&#34;&gt;https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-postfix-on-ubuntu-16-04&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&#34;setting-things-up-to-only-email-on-error&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Setting Things Up to Only Email on Error&lt;/h2&gt;
&lt;p&gt;Now you may be tempted to simply add your email address and call it a day, but
you might notice that R starts flooding your inbox with cron emails even for
successful runs of your script. If you prefer to only receive emails when your
script errors out, then you have to short circuit how cron identifies R script
errors. Below I’ll outline how to setup cron to only email on actual R script
errors.&lt;/p&gt;
&lt;p&gt;Cron determines whether to send an email based on the “exit code” of the script
that runs. An exit code is a number emitted at the end of the script indicating
a status of whether not an error occurred during the execution of the script.
However, R emits exit codes a little differently than most scripting languages.
In R, any messages are converted to &lt;code&gt;STDERR&lt;/code&gt; (signaling an error), so it is
possible to have a script that ran successfully, but signals an error occurred
in running the script. Upon realizing that R treats messages as errors in
disguise, it makes more sense to me why they are displayed in red in the R console:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/automating-r-scripts-with-cron/message-example.png&#34; style=&#34;display:block;margin:auto;max-height:100px;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Fortunately, a clever use of I/O redirection (similar to dplyr piping), can roll
message-induced error codes into less benign forms (&lt;code&gt;STDOUT&lt;/code&gt;) so that an email is
not triggered when messages were printed and your script really didn’t error
out. The way to do this is modify the command listed in your crontab file.
Instead of just including &lt;code&gt;Rscript /home/my-user/random.R&lt;/code&gt;, you should include:&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;Rscript /home/my-user/random.R &amp;gt; temp.log 2&amp;gt;&amp;amp;1 || cat temp.log&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;&amp;gt;&lt;/code&gt; symbol will redirect the error feed(&lt;code&gt;STDERR&lt;/code&gt; - captured by “2”) and roll
it into the standard output feed (&lt;code&gt;STDOUT&lt;/code&gt; - captured by “1”) and push them into
temp.log. The &lt;code&gt;||&lt;/code&gt; symbol checks whether the script had a non-zero exit status.
If so, then it will run the &lt;code&gt;cat&lt;/code&gt; command and print everything to the console
and trigger an error email from cron.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;emailing-errors-but-logging-all-runs&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Emailing Errors, but Logging All Runs&lt;/h2&gt;
&lt;p&gt;Finally, you might want to log the script output regardless of whether the
script errored out. To do tha you just need to continue redirecting the output that
you want to appear in the log file. The command below method will still roll
error messages into the standard feed and only trigger the email if the script
didn’t finish, but it will also push everything into a persistent log that will
keep a record of every run that you scripts do. This way you can get the error
emails, but still keep a complete log of everything your script did if you want
to have a complete log of all runs (successful or not).&lt;/p&gt;
&lt;pre class=&#34;shell&#34;&gt;&lt;code&gt;Rscript /home/my-user/random.R &amp;gt; temp.log 2&amp;gt;&amp;amp;1 || cat temp.log &amp;amp;&amp;amp; cat temp.log &amp;gt;&amp;gt; persistent.log &amp;amp;&amp;amp; rm temp.log&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s it. Now that you’re aware of this command you can swap out the R script
name and log names for every other script that you want to schedule. You really
don’t need to understand all of the details, just the last snippet of code in
this post, but I would recommend browsing online to understand more about cron
and I/O redirection. &lt;strong&gt;Note&lt;/strong&gt;: I’ve outlined this logic in a Stack Overflow
response at &lt;a href=&#34;https://stackoverflow.com/a/34442846/5258043&#34; class=&#34;uri&#34;&gt;https://stackoverflow.com/a/34442846/5258043&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>A Scalable Predictive API with PFA and AWS Lambda</title>
      <link>https://stevenmortimer.com/a-scalable-predictive-api-with-pfa-and-aws-lambda/</link>
      <pubDate>Fri, 28 Jul 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/a-scalable-predictive-api-with-pfa-and-aws-lambda/</guid>
      <description>
        

&lt;div id=&#34;TOC&#34;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#a-scalable-infrastructure&#34;&gt;A Scalable Infrastructure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#model-building-and-execution-using-pfa&#34;&gt;Model Building and Execution using PFA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#actually-doing-this&#34;&gt;Actually Doing This&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#step-1---creating-the-lambda-function&#34;&gt;Step 1 - Creating the Lambda Function&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-2---build-your-model&#34;&gt;Step 2 - Build Your Model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-3---upload-your-model-to-s3&#34;&gt;Step 3 - Upload Your Model to S3&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-4---test-your-lambda-makes-predictions&#34;&gt;Step 4 - Test Your Lambda Makes Predictions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#step-5---hook-lambda-up-to-api-gateway&#34;&gt;Step 5 - Hook Lambda Up to API Gateway&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#load-testing&#34;&gt;Load Testing&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#load-testing-results&#34;&gt;Load Testing Results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#load-testing-config&#34;&gt;Load Testing Config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;Building machine learning models is fun and challenging, but the real challenge can
be figuring out how to deploy your models in a production environment. This includes
answering questions like: Does the model need to be translated to another programming
language? Will it scale? Does it need to be dumbed down? Usually, the person who
built the model is not an expert on how to deploy the model in infrastructure,
which has led to a proliferation of turn-key solutions for converting machine
learning models into web APIs:
&lt;a href=&#34;https://azure.microsoft.com/en-us/services/machine-learning/&#34; target=&#34;_blank&#34;&gt;Azure ML&lt;/a&gt;,
&lt;a href=&#34;https://www.yhat.com/products/scienceops&#34; target=&#34;_blank&#34;&gt;YHat ScienceOps&lt;/a&gt;,
&lt;a href=&#34;https://www.dominodatalab.com/solutions/&#34; target=&#34;_blank&#34;&gt;Domino Data Lab&lt;/a&gt;,
and many others. I’m not a big fan of spending money on these types of platforms or
systems because they are usually costly and require learning quirks of a specific
platform that has limited features and support, so I’ve tinkered with finding alternatives.&lt;/p&gt;
&lt;div id=&#34;a-scalable-infrastructure&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;A Scalable Infrastructure&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://aws.amazon.com/lambda/&#34; target=&#34;_blank&#34;&gt;AWS Lambda&lt;/a&gt; is
serverless computing, in other words, you only pay for the computation of a function
you’ve uploaded to AWS. AWS will manage the orchestration of servers to handle requests.
AWS Lambda functions can easily be turned into a webservice using AWS API Gateway, so this accomplishes
your accessibility and scalability requirements. AWS will scale to however many requests
come in and you only pay for what you use. The question is: How to execute machine
learning models from an AWS Lambda function?&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;model-building-and-execution-using-pfa&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Model Building and Execution using PFA&lt;/h2&gt;
&lt;p&gt;Models can be built in many languages like, Python, R, or Java and if you work on a large,
cross-functional team it might not be possible to agree on using just one language.
The &lt;a href=&#34;http://dmg.org/pfa/&#34; target=&#34;_blank&#34;&gt;Portable Format for Analytics (PFA)&lt;/a&gt;
is an interesting solution because it is a language-agnostic, plain-text standard
for representing analytical workflows. PFA can be executed from within an AWS Lambda function in
either a Java or Python runtime using the Hadrian or Titus libraries, respectively (an example is provided below).
At a high-level data will be sent via HTTP to API Gateway, which provides
scaffolding to route the request to your AWS Lambda function. Once the data reaches the
function, the PFA logic will be applied and the result passed back to complete the
roundtrip API call. All of this is completely scalable, can be run at low cost, and
automatically gives you infrastructure for tracking response times, assigning per-user
access keys, creating API documentation, applying request validation and much more.
As a bonus, using AWS may also be more likely to get more support from your IT staff
should you need it.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;actually-doing-this&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Actually Doing This&lt;/h2&gt;
&lt;div id=&#34;step-1---creating-the-lambda-function&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 1 - Creating the Lambda Function&lt;/h3&gt;
&lt;p&gt;The absolute simplest test would be to clone the
&lt;a href=&#34;https://github.com:StevenMMortimer/pfa-lambda&#34; target=&#34;_blank&#34;&gt;repository&lt;/a&gt; where I’ve created a
Lambda deploy package for running Titus (Python runtime), then zip up that package
and upload to the AWS Lambda console.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone git@github.com:StevenMMortimer/pfa-lambda.git
zip -r my-lambda-deploy.zip .&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/upload-deploy-package.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Note, you’ll want to select the Runtime for your lambda as Python 2.7 since that
is the Python version that this lambda deploy package was created. Also, the deploy
package has a function file named &lt;code&gt;lambda_function.py&lt;/code&gt; so that it matches
the handler specification in the AWS console by default (i.e. The filename.handler-method
value in your function.). You could rename your &lt;code&gt;.py&lt;/code&gt; file as anything, but remember to
update the handler specification to match.&lt;/p&gt;
&lt;p&gt;Finally, the Lambda function runs under an IAM service role. If you’d like the Lambda
function to access other AWS resources, like reading your model file from S3, then
you’ll want to make sure that the service role as permissions to access S3. For example,
the image below shows the service role has access to write to CloudFront logs, but
also full access to S3.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/service-role-permissions1.png&#34; /&gt;
&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/service-role-permissions2.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Proceed through the create Lambda function dialog to create your function, then give it a test. The function
&lt;code&gt;lambda_function.py&lt;/code&gt; has hand-coded PFA logic to take 3.14 and add 100 to it, which returns 103.14.
In the test summary, you can see the output as 103.14, but you can also see that
the function executed in .25 milliseconds. A machine learning model executing in
.25 milliseconds is pretty darn fast and exciting to see, but this is just a toy example.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/simple-lambda-test-result.png&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-2---build-your-model&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 2 - Build Your Model&lt;/h3&gt;
&lt;p&gt;A more realistic example, might be building a Naive Bayes classifier on a dataset of with
many predictors. The &lt;code&gt;mlbench&lt;/code&gt; package has a dataset called “Sonar” that consists of
208 data points collected on 60 predictors. Our goal will be to predict two classes
(M for metal cylinder or R for rock). In the code below, I create a model and export
it to PFA using the &lt;code&gt;aurelius&lt;/code&gt; package.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(mlbench)
library(e1071)
data(Sonar)
set.seed(1)
sonar_model &amp;lt;- naiveBayes(Class ~ ., data=Sonar)

library(aurelius) # library for converting to PFA
sonar_model_as_pfa &amp;lt;- pfa(sonar_model)
write_pfa(sonar_model_as_pfa, &amp;#39;~/model.pfa&amp;#39;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that the model could be created in
&lt;a href=&#34;http://scikit-learn.org/stable/&#34;&gt;scikit-learn&lt;/a&gt;,
&lt;a href=&#34;%22https://spark.apache.org/mllib/%22&#34;&gt;MLlib&lt;/a&gt; or any number of
&lt;a href=&#34;http://www.kdnuggets.com/2015/06/top-20-r-machine-learning-packages.html&#34;&gt;R packages for maching learning&lt;/a&gt;
and translated using a PFA converter library. Support exists for the JVM, Python, and R,
just check &lt;a href=&#34;https://github.com/opendatagroup/hadrian&#34; target=&#34;_blank&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-3---upload-your-model-to-s3&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 3 - Upload Your Model to S3&lt;/h3&gt;
&lt;p&gt;After you’ve created a PFA model upload it to an S3 bucket. In the Lambda deploy package
there is a Lambda function called &lt;code&gt;model-from-s3.py&lt;/code&gt; that has logic to read a PFA model
from an S3 bucket and use it for scoring.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/upload-model-to-s3.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Note, you may have to adjust the bucket name and model file name in the Python
script to read the file you’ve uploaded. These parameters are specified in &lt;a href=&#34;https://github.com/StevenMMortimer/pfa-lambda/blob/master/model-from-s3.py#L10-L11&#34;&gt;Lines 10
and 11&lt;/a&gt;
in the script.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-4---test-your-lambda-makes-predictions&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 4 - Test Your Lambda Makes Predictions&lt;/h3&gt;
&lt;p&gt;After configuring the script &lt;code&gt;model-from-s3.py&lt;/code&gt; and re-uploading your Lambda deploy package,
you will need to update the handler specification in the AWS console. Change the value
to be &lt;code&gt;model-from-s3.lambda_handler&lt;/code&gt;. This model requires the data to be provided
via the event JSON for the function. You can use the AWS Hello World testing template
which provides 3 dummy key-value pairs. Our model had 60 variables as inputs so instead of
writing those out by hand, it’s easiest to pull out one record from the sample data and
copy/paste into the testing template.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;jsonlite::toJSON(jsonlite::unbox(Sonar[1,grepl(&amp;#39;V[0-9]+&amp;#39;, names(Sonar))]))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## {&amp;quot;V1&amp;quot;:0.02,&amp;quot;V2&amp;quot;:0.0371,&amp;quot;V3&amp;quot;:0.0428,&amp;quot;V4&amp;quot;:0.0207,&amp;quot;V5&amp;quot;:0.0954,&amp;quot;V6&amp;quot;:0.0986,&amp;quot;V7&amp;quot;:0.1539,&amp;quot;V8&amp;quot;:0.1601,&amp;quot;V9&amp;quot;:0.3109,&amp;quot;V10&amp;quot;:0.2111,&amp;quot;V11&amp;quot;:0.1609,&amp;quot;V12&amp;quot;:0.1582,&amp;quot;V13&amp;quot;:0.2238,&amp;quot;V14&amp;quot;:0.0645,&amp;quot;V15&amp;quot;:0.066,&amp;quot;V16&amp;quot;:0.2273,&amp;quot;V17&amp;quot;:0.31,&amp;quot;V18&amp;quot;:0.2999,&amp;quot;V19&amp;quot;:0.5078,&amp;quot;V20&amp;quot;:0.4797,&amp;quot;V21&amp;quot;:0.5783,&amp;quot;V22&amp;quot;:0.5071,&amp;quot;V23&amp;quot;:0.4328,&amp;quot;V24&amp;quot;:0.555,&amp;quot;V25&amp;quot;:0.6711,&amp;quot;V26&amp;quot;:0.6415,&amp;quot;V27&amp;quot;:0.7104,&amp;quot;V28&amp;quot;:0.808,&amp;quot;V29&amp;quot;:0.6791,&amp;quot;V30&amp;quot;:0.3857,&amp;quot;V31&amp;quot;:0.1307,&amp;quot;V32&amp;quot;:0.2604,&amp;quot;V33&amp;quot;:0.5121,&amp;quot;V34&amp;quot;:0.7547,&amp;quot;V35&amp;quot;:0.8537,&amp;quot;V36&amp;quot;:0.8507,&amp;quot;V37&amp;quot;:0.6692,&amp;quot;V38&amp;quot;:0.6097,&amp;quot;V39&amp;quot;:0.4943,&amp;quot;V40&amp;quot;:0.2744,&amp;quot;V41&amp;quot;:0.051,&amp;quot;V42&amp;quot;:0.2834,&amp;quot;V43&amp;quot;:0.2825,&amp;quot;V44&amp;quot;:0.4256,&amp;quot;V45&amp;quot;:0.2641,&amp;quot;V46&amp;quot;:0.1386,&amp;quot;V47&amp;quot;:0.1051,&amp;quot;V48&amp;quot;:0.1343,&amp;quot;V49&amp;quot;:0.0383,&amp;quot;V50&amp;quot;:0.0324,&amp;quot;V51&amp;quot;:0.0232,&amp;quot;V52&amp;quot;:0.0027,&amp;quot;V53&amp;quot;:0.0065,&amp;quot;V54&amp;quot;:0.0159,&amp;quot;V55&amp;quot;:0.0072,&amp;quot;V56&amp;quot;:0.0167,&amp;quot;V57&amp;quot;:0.018,&amp;quot;V58&amp;quot;:0.0084,&amp;quot;V59&amp;quot;:0.009,&amp;quot;V60&amp;quot;:0.0032}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/real-model-test-inputs.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The results of a test on the real model are also encouraging. The prediction was
“R”, which matches the prediction of “R” using &lt;code&gt;predict(sonar_model, Sonar[1,])&lt;/code&gt;.
Also, it only took 52.46 milliseconds to execute against a Naive Bayes model
model with 60 input variables. Note, different models will have different latencies,
so your performance may vary.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/real-model-results.png&#34; /&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;step-5---hook-lambda-up-to-api-gateway&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Step 5 - Hook Lambda Up to API Gateway&lt;/h3&gt;
&lt;p&gt;The last step in making your prediction service is to open it up as a
webservice so that other applications can take advantage of your hard work. I won’t
go into detail about &lt;a href=&#34;https://aws.amazon.com/api-gateway/&#34; target=&#34;_blank&#34;&gt;API Gateway&lt;/a&gt;,
but it’s very powerful in terms of supporting a full-fledged API (access keys,
logging, documentation, etc.). I would encourage you to take advantage of these
after getting the bare bones of your API working.&lt;/p&gt;
&lt;p&gt;First, create an API and add a POST method. You’ll want to use a POST method
because new data for your prediction will be POSTed in the request body as JSON.
While setting up the method you can associate it to your Lambda function. You’ll
be able to test that your API works by supplying some sample JSON in a format that
your model expects.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/create-api-method.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Finally, API Gateway requires you to “Deploy” your API so you’ll need to do that
step before being able to access it via the web. APIs are deployed in stages, so you
can create a stage called “dev” and deploy to it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/a-scalable-predictive-api-with-pfa-and-aws-lambda/deploy-api.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Now that everything is setup you can test calls against the invoke url by submitting
a &lt;code&gt;curl&lt;/code&gt; command, like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST -H &amp;quot;Content-Type: application/json&amp;quot; -d &amp;#39;{&amp;quot;V1&amp;quot;:0.02,&amp;quot;V2&amp;quot;:0.0371,&amp;quot;V3&amp;quot;:0.0428,&amp;quot;V4&amp;quot;:0.0207,&amp;quot;V5&amp;quot;:0.0954,&amp;quot;V6&amp;quot;:0.0986,&amp;quot;V7&amp;quot;:0.1539,&amp;quot;V8&amp;quot;:0.1601,&amp;quot;V9&amp;quot;:0.3109,&amp;quot;V10&amp;quot;:0.2111,&amp;quot;V11&amp;quot;:0.1609,&amp;quot;V12&amp;quot;:0.1582,&amp;quot;V13&amp;quot;:0.2238,&amp;quot;V14&amp;quot;:0.0645,&amp;quot;V15&amp;quot;:0.066,&amp;quot;V16&amp;quot;:0.2273,&amp;quot;V17&amp;quot;:0.31,&amp;quot;V18&amp;quot;:0.2999,&amp;quot;V19&amp;quot;:0.5078,&amp;quot;V20&amp;quot;:0.4797,&amp;quot;V21&amp;quot;:0.5783,&amp;quot;V22&amp;quot;:0.5071,&amp;quot;V23&amp;quot;:0.4328,&amp;quot;V24&amp;quot;:0.555,&amp;quot;V25&amp;quot;:0.6711,&amp;quot;V26&amp;quot;:0.6415,&amp;quot;V27&amp;quot;:0.7104,&amp;quot;V28&amp;quot;:0.808,&amp;quot;V29&amp;quot;:0.6791,&amp;quot;V30&amp;quot;:0.3857,&amp;quot;V31&amp;quot;:0.1307,&amp;quot;V32&amp;quot;:0.2604,&amp;quot;V33&amp;quot;:0.5121,&amp;quot;V34&amp;quot;:0.7547,&amp;quot;V35&amp;quot;:0.8537,&amp;quot;V36&amp;quot;:0.8507,&amp;quot;V37&amp;quot;:0.6692,&amp;quot;V38&amp;quot;:0.6097,&amp;quot;V39&amp;quot;:0.4943,&amp;quot;V40&amp;quot;:0.2744,&amp;quot;V41&amp;quot;:0.051,&amp;quot;V42&amp;quot;:0.2834,&amp;quot;V43&amp;quot;:0.2825,&amp;quot;V44&amp;quot;:0.4256,&amp;quot;V45&amp;quot;:0.2641,&amp;quot;V46&amp;quot;:0.1386,&amp;quot;V47&amp;quot;:0.1051,&amp;quot;V48&amp;quot;:0.1343,&amp;quot;V49&amp;quot;:0.0383,&amp;quot;V50&amp;quot;:0.0324,&amp;quot;V51&amp;quot;:0.0232,&amp;quot;V52&amp;quot;:0.0027,&amp;quot;V53&amp;quot;:0.0065,&amp;quot;V54&amp;quot;:0.0159,&amp;quot;V55&amp;quot;:0.0072,&amp;quot;V56&amp;quot;:0.0167,&amp;quot;V57&amp;quot;:0.018,&amp;quot;V58&amp;quot;:0.0084,&amp;quot;V59&amp;quot;:0.009,&amp;quot;V60&amp;quot;:0.0032}&amp;#39; https://7uc1muh9pd.execute-api.us-east-1.amazonaws.com/dev&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;load-testing&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Load Testing&lt;/h2&gt;
&lt;p&gt;For those of you who are interested in the scalability. I used
&lt;a href=&#34;https://artillery.io&#34; target=&#34;_blank&#34;&gt;artillery.io&lt;/a&gt; to load test and
got the following results for our classifier. I created a test where the load
simulated 10 requests per second over 60 seconds. The 99th percentile latency was
0.30 seconds and the median was 0.15 seconds. The complete results were:&lt;/p&gt;
&lt;div id=&#34;load-testing-results&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Load Testing Results&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;all scenarios completed
Complete report @ 2017-07-28T16:53:05.387Z
  Scenarios launched:  600
  Scenarios completed: 600
  Requests completed:  600
  RPS sent: 9.93
  Request latency:
    min: 115.6
    max: 806.8
    median: 149.1
    p95: 217.3
    p99: 300.1
  Scenario duration:
    min: 116.6
    max: 807.8
    median: 150.5
    p95: 219.6
    p99: 301
  Scenario counts:
    0: 600 (100%)
  Codes:
    200: 600&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;load-testing-config&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Load Testing Config&lt;/h3&gt;
&lt;p&gt;Here is my testing config as artillery YAML file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config:
  target: &amp;#39;https://7uc1muh9pd.execute-api.us-east-1.amazonaws.com&amp;#39;
  phases:
    - duration: 60
      arrivalRate: 10
  defaults:
    headers:
      Content-Type: &amp;#39;application/json&amp;#39;
  payload:
    path: &amp;quot;./test-data.csv&amp;quot;
    fields:
      - &amp;quot;V1&amp;quot;
      - &amp;quot;V2&amp;quot;
      - &amp;quot;V3&amp;quot;
      - &amp;quot;V4&amp;quot;
      - &amp;quot;V5&amp;quot;
      - &amp;quot;V6&amp;quot;
      - &amp;quot;V7&amp;quot;
      - &amp;quot;V8&amp;quot;
      - &amp;quot;V9&amp;quot;
      - &amp;quot;V10&amp;quot;
      - &amp;quot;V11&amp;quot;
      - &amp;quot;V12&amp;quot;
      - &amp;quot;V13&amp;quot;
      - &amp;quot;V14&amp;quot;
      - &amp;quot;V15&amp;quot;
      - &amp;quot;V16&amp;quot;
      - &amp;quot;V17&amp;quot;
      - &amp;quot;V18&amp;quot;
      - &amp;quot;V19&amp;quot;
      - &amp;quot;V20&amp;quot;
      - &amp;quot;V21&amp;quot;
      - &amp;quot;V22&amp;quot;
      - &amp;quot;V23&amp;quot;
      - &amp;quot;V24&amp;quot;
      - &amp;quot;V25&amp;quot;
      - &amp;quot;V26&amp;quot;
      - &amp;quot;V27&amp;quot;
      - &amp;quot;V28&amp;quot;
      - &amp;quot;V29&amp;quot;
      - &amp;quot;V30&amp;quot;
      - &amp;quot;V31&amp;quot;
      - &amp;quot;V32&amp;quot;
      - &amp;quot;V33&amp;quot;
      - &amp;quot;V34&amp;quot;
      - &amp;quot;V35&amp;quot;
      - &amp;quot;V36&amp;quot;
      - &amp;quot;V37&amp;quot;
      - &amp;quot;V38&amp;quot;
      - &amp;quot;V39&amp;quot;
      - &amp;quot;V40&amp;quot;
      - &amp;quot;V41&amp;quot;
      - &amp;quot;V42&amp;quot;
      - &amp;quot;V43&amp;quot;
      - &amp;quot;V44&amp;quot;
      - &amp;quot;V45&amp;quot;
      - &amp;quot;V46&amp;quot;
      - &amp;quot;V47&amp;quot;
      - &amp;quot;V48&amp;quot;
      - &amp;quot;V49&amp;quot;
      - &amp;quot;V50&amp;quot;
      - &amp;quot;V51&amp;quot;
      - &amp;quot;V52&amp;quot;
      - &amp;quot;V53&amp;quot;
      - &amp;quot;V54&amp;quot;
      - &amp;quot;V55&amp;quot;
      - &amp;quot;V56&amp;quot;
      - &amp;quot;V57&amp;quot;
      - &amp;quot;V58&amp;quot;
      - &amp;quot;V59&amp;quot;
      - &amp;quot;V60&amp;quot;     

scenarios:
  - flow:
    - post:
        url: &amp;quot;/dev&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Google Sites for Documentation</title>
      <link>https://stevenmortimer.com/google-sites-for-documentation/</link>
      <pubDate>Fri, 02 Jun 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/google-sites-for-documentation/</guid>
      <description>
        

&lt;div id=&#34;TOC&#34;&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#the-wysiwyg&#34;&gt;The WYSIWYG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#access-control-permissions&#34;&gt;Access Control Permissions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#embedded-google-docs-sheets-etc.&#34;&gt;Embedded Google Docs, Sheets, etc.&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#built-in-google-search-and-analytics&#34;&gt;Built In Google Search and Analytics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#final-thoughts&#34;&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;The title of this post is probably off-putting to a large number of people. Why
on earth would I use Google Sites for blogging, wiki, and technical
documentation? Isn’t that old, outdated tech even though Google did release a
newer version? To be honest, I agree with those objections. This blog isn’t
written with Google Sites. It’s written using the R package &lt;code&gt;blogdown&lt;/code&gt; and Hugo
and they are fantastic. However, there are some features of Google Sites that
might make it a nice tool for you and your work colleagues.&lt;/p&gt;
&lt;div id=&#34;the-wysiwyg&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;The WYSIWYG&lt;/h2&gt;
&lt;p&gt;Never underestimate the power of the WYSIWYG editor. This magical tool allows
anybody, at any technical skill level, to contribute content. Creating and
maintaing a good website requires constant updates and tinkering that a single
person might not be able to keep up with. The graphical interface allows you to
share work with non-technical contributors as they shore up and support your
efforts.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;access-control-permissions&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Access Control Permissions&lt;/h2&gt;
&lt;p&gt;Static website generators are nice, but lack some more robust and intutive tools
for granting permissions and access to your site. With Google Sites this comes
standard. If your orgnization uses the Google Suite, this is one of the main
reasons for sticking to Google Sites. You can contol view and edit permissions
to any level of detail for individuals inside and outside of your organization.
Again, this can all be with the click of a button or API calls, just take your
pick.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;embedded-google-docs-sheets-etc.&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Embedded Google Docs, Sheets, etc.&lt;/h2&gt;
&lt;p&gt;Not sure if this needs to be said, but adding it here for completeness. Google
Sites plays well with embedding Docs, Sheets, Slides, and other Google products.
If you already have a significant amount of content in these formats, you’ll
find it easy to integrate into a single source.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;built-in-google-search-and-analytics&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Built In Google Search and Analytics&lt;/h2&gt;
&lt;p&gt;Your Google Site automatically comes with the powerful and handy Google Search
technology. A good turn-key search solution on your website is critical,
especially for wikis and technical documents. Users typically won’t frequent
your site unless they are looking for something. The search bar is their window
into the trove of information you’ve got. The search makes finding the content
they need easy and encourages them to come back for their next quesion. User
engagement fosters the virtuous circle where the contributors add more content
and users consume more and more of it. And, of course, Google Sites makes it
easy to plug in your Google Analytics code and track that user engagement.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;final-thoughts&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Google Sites are alright in my book. I’ve used them and it’s worked out well,
but each time I had a strong reason for wanting to use it and was committed from
the beginning. Given my experience with R and willingness to use the Sites API I
went ahead and created a collection of functions that make it easier to create,
update, and delete content on your Google Site without ever leaving R. I’ve
started bundling together the functions into an R packge called &lt;code&gt;googlesites&lt;/code&gt;
that’s currently on my GitHub. Please take a look to see the sorts of things you
can do with the package at
&lt;a href=&#34;https://github.com/StevenMMortimer/googlesites&#34; target=&#34;_blank&#34;&gt;https://github.com/StevenMMortimer/googlesites&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>An R User Chooses a Python IDE</title>
      <link>https://stevenmortimer.com/an-r-user-chooses-a-python-ide/</link>
      <pubDate>Fri, 07 Apr 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/an-r-user-chooses-a-python-ide/</guid>
      <description>
        


&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/an-r-user-chooses-a-python-ide/choose-an-ide.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UPDATE - July 2020&lt;/strong&gt;: As of last summer (June 2019), I have switched to VS
Code as a Python IDE and never looked back. The reason being that it feels a
little bit more lightweight than PyCharm, but still has all the functionality
one would need to write Python code and can be more easily configured to act
like RStudio. Check out my more recent post &lt;a target=&#34;_blank&#34;
href=&#34;https://stevenmortimer.com/setting-up-vs-code-for-python-development-like-rstudio/&#34;&gt;Setting up VS Code for Python Development like RStudio&lt;/a&gt; for my &lt;code&gt;settings.json&lt;/code&gt; (user settings) and
&lt;code&gt;keybindings.json&lt;/code&gt; (shortcuts) configuration files that you can download and paste
into your own VS Code installation to get up and running quickly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Yes, this is a post about IDEs. I don’t intend to insult or incite any ill-will
towards any person or project. The purpose of this post is to give other people
who are familiar working with R, some insight into how a fellow R user has evaluated
and experienced picking an IDE for programming Python. My criteria was to have an
interactive experience, like RStudio, with support for debugging, version control,
and code completion. That’s not too much to ask for, right?!&lt;/p&gt;
&lt;div id=&#34;jupyter-notebooks&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Jupyter Notebooks&lt;/h2&gt;
&lt;p&gt;If you have been interested in data science, and R and Python programming, then
you’ve probably seen IPython or the newer
&lt;a href=&#34;http://jupyter.org&#34; target=&#34;_blank&#34;&gt;Jupyter&lt;/a&gt; Notebooks. You might have also seen
something similar with Kaggle Kernels or Zeppelin Notebooks. I agree that the notebook
format is great for explaining and teaching, which is why so many tutorials display
their work in notebook format. The problem with Jupyter notebooks is that the format
is plain-text, but not easily readable when checking code diffs on a version control
system like GitHub. For example, a simple code change can make massive diffs:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/an-r-user-chooses-a-python-ide/jupyter-commit-diff.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Second, notebooks force you to work in chunks. Chunks are nice for helping you
separate and structure your workflow, but you can’t run half a chunk. It’s an
all-or-nothing game. The problems occur when you need to debug something in the middle
of a chunk. At that point you need to deconstruct the chunk, then test it, then put it back together.
It’s a mess. Finally, when you move your code into production it likely won’t be in a
notebook format. Why write code in a notebook when you know it will probably be
rewritten again in another format? This is inviting silly errors and wasted time.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;rodeo&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Rodeo&lt;/h2&gt;
&lt;p&gt;If you’ve used any other tools created by &lt;a href=&#34;https://www.yhat.com&#34; target=&#34;_blank&#34;&gt;YHat&lt;/a&gt;
or just want the RStudio exprience with Python, then you might be tempted to use
&lt;a href=&#34;https://www.yhat.com/products/rodeo&#34; target=&#34;_blank&#34;&gt;Rodeo&lt;/a&gt;. Rodeo
does a fantastic job at replicating the look and feel of RStudio, but if you take a
closer look it is missing a lot of features you use in RStudio, which is frustrating.
You can tell even from screenshots that it’s seriously lagging RStudio in features.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/an-r-user-chooses-a-python-ide/rodeo-ide-screenshot.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;Rodeo is open-source, but development is mainly driven by the folks at YHat, so
support and longevitiy is a concern. Finally, it does not have native integration with
Git, which is a deal-breaker. If you can’t version control your code easily, then you’re
more likely to make mistakes.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;spyder&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Spyder&lt;/h2&gt;
&lt;p&gt;I can’t comment much on my experience with
&lt;a href=&#34;https://pythonhosted.org/spyder/&#34; target=&#34;_blank&#34;&gt;Spyder&lt;/a&gt;.
I had an issue with converting a notebook to a script, which was a feature I
hoped would work effortlessly. Also, it doesn’t support Git, which as mentioned with
Rodeo, is an immediate deal-breaker. Spyder seems good for a specific data science
niche, but it’s not quite there, and with so many other full-fledged, battle-tested
Python IDEs, why not expand your horizons to have an IDE that will allow you to
utilize the entire spectrum of general purpose programming that Python is?&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;text-editor-e.g.-sublime&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Text Editor (e.g. Sublime)&lt;/h2&gt;
&lt;p&gt;If you ask a hardcore programmer about picking an IDE, they might tell you that
you don’t need one at all. It’s true that you can effectively and efficiently write
Python code from a text editor; however, I wouldn’t recommend an R user take this
approach. First, you’ll probably be frustrated with the lack of transparency for
defined variables, folder structure, etc. that RStudio neatly provides in its
“Environment” and “Files” panes. Second, if you’re just starting to learn Python
after learning R, you’ll want some more guardrails to prevent against syntax errors,
code completion, formatting, etc. This isn’t to say that text editors don’t provide it,
because they do, but you’ll spend time configuring these features inside of getting them
out of the box from an IDE.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;pycharm&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;PyCharm&lt;/h2&gt;
&lt;p&gt;PyCharm definitely has the feel of a heavier IDE. There are a lot of settings
and features that would be intimidating at first, but it’s got everything right
there. A console, version control support, debugging, code completion and more. The
one thing I did change was the keyboard shortcuts to executing code interactively
especially when highlighting code sections to get that same feeling as you get in
RStudio where you can select pretty much anything and get it to run down in the
console interactively. After tweaking the keyboard shortcuts, it was pretty much the same
flow as using RStudio and I felt good about using a full-featured and well supported
project. Ultimately, PyCharm is what I’m sticking with to continue writing Python code.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;final-thoughts&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;I have read quite a bit online about the IDEs mentioned above, but other peoples’
opinions didn’t do much for me. I literally downloaded each IDE and spent a couple
hours trying to work on a project. If it was easy to get going I kept trying more
things until I was able to test out my desired features. You can take my word
for going with PyCharm to continue your Pythonic data science, but I’d recommend
to anyone to try things out, see what you like, and go with it. In the end, you should
be spending the majority of your efforts writing code, not fiddling with the IDE,
so make the choice that makes you most happy and productive.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>PFA with the R Package aurelius</title>
      <link>https://stevenmortimer.com/pfa-with-the-r-package-aurelius/</link>
      <pubDate>Mon, 13 Feb 2017 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/pfa-with-the-r-package-aurelius/</guid>
      <description>
        


&lt;p&gt;I first heard about the Portable Format for Analytics (PFA) through KDnuggets in
2016. There was a &lt;a href=&#34;http://www.kdnuggets.com/2016/01/portable-format-analytics-models-production.html&#34; target=&#34;_blank&#34;&gt;post&lt;/a&gt; that in vague way talked about the virtues and benefits of PFA, but not to completely
supplant PMML, which I was already familiar with. PMML was pretty cool, but I
couldn’t find good support and felt that the XML-based approach was clumsy and
outdated so when I heard about PFA, I was pretty excited. A new JSON-based standard
for encoding any type of analytics, not just certain supported models like PMML.
I mainly work in the R programming language, so I was disappointed to find resources
lacking that would convert models made in R into the PFA standard. The great
folks at the &lt;a href=&#34;http://dmg.org&#34; target=&#34;_blank&#34;&gt;Data Mining Group&lt;/a&gt; (DMG)
had started on an R package called &lt;code&gt;aurelius&lt;/code&gt; that contained the bare bones functions
to construct the syntax. After reading through the Project pages on GitHub, I saw
everything was there, but it wasn’t as convenient as I would like so I started
writing “model producers”. Model producers are functions that convert model objects
in R into their PFA equivalent to be exported and used in other systems. In this
process I learned a great deal. I thought I knew how models worked, but actually
reading through the mechanics of scoring has made me appreciate them so much more.
Below is an outline of working with the &lt;code&gt;aurelius&lt;/code&gt; package, but note that this work
has yet to be integrated into the project and exists on forked branch in my GitHub
(installation command shown below).&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# you only need to run this once because it 
# installs the package
devtools::install_github(&amp;#39;StevenMMortimer/hadrian&amp;#39;, 
                         ref=&amp;#39;feature/add-r-package-structure&amp;#39;, 
                         subdir=&amp;#39;aurelius&amp;#39;, 
                         quick=T) &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example we’ll load the randomForest package to demonstrate how to convert
randomForest models into PFA. Currently, the package converts models of many types:
&lt;code&gt;lm&lt;/code&gt;, &lt;code&gt;glm&lt;/code&gt;, &lt;code&gt;glmnet&lt;/code&gt;, &lt;code&gt;gbm&lt;/code&gt;, &lt;code&gt;randomForest&lt;/code&gt;, &lt;code&gt;naivebayes&lt;/code&gt;, &lt;code&gt;lda&lt;/code&gt;, &lt;code&gt;kcca&lt;/code&gt;, and &lt;code&gt;kmeans&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;options(stringsAsFactors = FALSE)
library(aurelius)
library(randomForest) &lt;/code&gt;&lt;/pre&gt;
&lt;div id=&#34;pfa-structure&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;PFA Structure&lt;/h2&gt;
&lt;p&gt;Before we get started it’s helpful to understand the basic components of a valid
PFA document. Every PFA document has an input spec, output spec, and an action.
The reason for the specifications on the input and output is so that the machine
executing your PFA can safely and reliably excute. PFA relies on the
&lt;a href=&#34;http://avro.apache.org/&#34; target=&#34;_blank&#34;&gt;Avro serialization format&lt;/a&gt;
for its type system, whichsupports your typical primitive data types like
boolean, integer, and string, but also, arrays, maps, and
&lt;a href=&#34;http://dmg.org/pfa/docs/avro_types/&#34; target=&#34;_blank&#34;&gt;more&lt;/a&gt;. You can
define these types in R by using any of the functions like &lt;code&gt;avro_*&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;avro_double
#&amp;gt; [1] &amp;quot;double&amp;quot;

avro_enum(list(&amp;quot;one&amp;quot;, &amp;quot;two&amp;quot;))
#&amp;gt; $type
#&amp;gt; [1] &amp;quot;enum&amp;quot;
#&amp;gt; 
#&amp;gt; $symbols
#&amp;gt; $symbols[[1]]
#&amp;gt; [1] &amp;quot;one&amp;quot;
#&amp;gt; 
#&amp;gt; $symbols[[2]]
#&amp;gt; [1] &amp;quot;two&amp;quot;
#&amp;gt; 
#&amp;gt; 
#&amp;gt; $name
#&amp;gt; [1] &amp;quot;Enum_1&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After specifying inputs and outputs, you must specify an action. With &lt;code&gt;aurelius&lt;/code&gt;
an action can be specified from an R expression. A simple example is &lt;code&gt;2+2&lt;/code&gt;. This
can be expanded to manipulate the input and/or derivatives of it. Here is a
simple analytics spec that takes a double and adds 10 to it.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;pfa_document(input = avro_double, 
             output = avro_double, 
             action = expression(input + 10))
#&amp;gt; $input
#&amp;gt; [1] &amp;quot;double&amp;quot;
#&amp;gt; 
#&amp;gt; $output
#&amp;gt; [1] &amp;quot;double&amp;quot;
#&amp;gt; 
#&amp;gt; $action
#&amp;gt; $action[[1]]
#&amp;gt; $action[[1]]$`+`
#&amp;gt; $action[[1]]$`+`[[1]]
#&amp;gt; [1] &amp;quot;input&amp;quot;
#&amp;gt; 
#&amp;gt; $action[[1]]$`+`[[2]]
#&amp;gt; [1] 10&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You’ll notice that the resulting output is a list. PFA is based on JSON, so it’s
natural for the document, when it’s in R, to be a list that can always later be
converted to JSON for export.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;converting-a-tree-model&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Converting a Tree Model&lt;/h2&gt;
&lt;p&gt;Let’s step through a more realistic example that predicts the species in the iris
dataset, based on the other variables using a random forest model. First, format
the columns to meet Avro guidelines: &lt;code&gt;[A-Za-z_][A-Za-z0-9_]*&lt;/code&gt;. Having periods
in the variable names (e.g. &lt;code&gt;Sepal.Length&lt;/code&gt;) is not encouraged.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# make sure column names dont have periods in them
iris2 &amp;lt;- iris
colnames(iris2) &amp;lt;- gsub(&amp;#39;\\.&amp;#39;, &amp;#39;_&amp;#39;, colnames(iris2)) &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Creating the PFA is very easy. Simply fit your model, then call &lt;code&gt;pfa()&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# fit a random forest model
rf_model &amp;lt;- randomForest(Species ~ ., data=iris2) 
 
# convert that model to PFA
# pred_type=&amp;#39;prob&amp;#39; means that the output is the 
# probability of all classes. If you want 
# the majority vote, then specify pred_type=&amp;#39;response&amp;#39;
# check all options by running ?pfa.randomForest
rf_model_as_pfa &amp;lt;- pfa(rf_model, pred_type=&amp;#39;prob&amp;#39;) &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you have the &lt;a href=&#34;https://cran.r-project.org/web/packages/rPython/index.html&#34; target=&#34;_blank&#34;&gt;rPython&lt;/a&gt; package
installed and &lt;a href=&#34;https://github.com/opendatagroup/hadrian/wiki/Installation#case-4-you-want-to-install-titus-in-python&#34; target=&#34;_blank&#34;&gt;Titus&lt;/a&gt; (PFA for Python), then you can create an engine in R
and see how the PFA behaves. In this case it predicts the same as the &lt;code&gt;predict()&lt;/code&gt;
function in R. This way we can test that the behavior of the PFA is what we expect.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# you can check the predictions from this model
# by first converting to a pfa_engine
pfa_model &amp;lt;- pfa_engine(rf_model_as_pfa) 
 
# confirm that the pfa engine produces the 
# same prediction as the &amp;quot;predict&amp;quot; method in R
test_dat &amp;lt;- iris2[73,1:4]
round(pfa_model$action(as.list(test_dat)), 6)
#&amp;gt;     setosa versicolor  virginica 
#&amp;gt;       0.00       0.83       0.17
round(unclass(predict(object=rf_model, newdata=test_dat, type=&amp;#39;prob&amp;#39;)), 6) 
#&amp;gt;    setosa versicolor virginica
#&amp;gt; 73      0       0.83      0.17&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once you’re happy with your model, just export to a flat file using &lt;code&gt;write_pfa()&lt;/code&gt;.
This format can be read and used by any PFA interpreter. With R you can read the
file back in using &lt;code&gt;read_pfa()&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# you can export your model for use in other systems
write_pfa(rf_model_as_pfa, file = &amp;#39;~/my_rf_model.pfa&amp;#39;)

my_model &amp;lt;- read_pfa(file(&amp;quot;~/my_rf_model.pfa&amp;quot;))

# see that all PFA components are there
names(my_model)
#&amp;gt; [1] &amp;quot;input&amp;quot;  &amp;quot;output&amp;quot; &amp;quot;action&amp;quot; &amp;quot;cells&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;final-thoughts&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;PFA is a measurable step in open, portable, and reprodcible analytics. I hope to
see the standard more widely adopted and tools developed to help others implement it.
One final thing: Check out the
&lt;a href=&#34;https://github.com/StevenMMortimer/hadrian/tree/feature/add-r-package-structure/aurelius/tests/testthat&#34; target=&#34;_blank&#34;&gt;unit tests&lt;/a&gt;.
I’ve recently added quite a bit of unit test coverage and these tests are an
excellent source of examples because they cover most all cases of utilizing the
package functions.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>Creating an RStudio Addin</title>
      <link>https://stevenmortimer.com/creating-an-rstudio-addin/</link>
      <pubDate>Sat, 10 Dec 2016 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/creating-an-rstudio-addin/</guid>
      <description>
        


&lt;p&gt;I never thought too much about RStudio addins, but I saw a few cool examples (
&lt;a href=&#34;https://github.com/jennybc/jadd&#34; target=&#34;_blank&#34;&gt;jadd&lt;/a&gt;,
&lt;a href=&#34;https://github.com/daattali/colourpicker&#34; target=&#34;_blank&#34;&gt;colorpicker&lt;/a&gt;, and
&lt;a href=&#34;https://github.com/daattali/addinslist&#34; target=&#34;_blank&#34;&gt;addinslist&lt;/a&gt;) and
decided to take a closer look. After a little research, it seemed easier than I thought.
It was easier because RStudio provided some excellent
&lt;a href=&#34;https://rstudio.github.io/rstudioaddins&#34; target=&#34;_blank&#34;&gt;documentation on addins&lt;/a&gt;
and it turns out that if you can write R code, then you can write an RStudio addin.&lt;/p&gt;
&lt;div id=&#34;my-idea&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;My Idea&lt;/h2&gt;
&lt;p&gt;Around the time I was looking at addins, I started taking advantage of a little
RStudio trick to navigate long scripts. If you create comments that are a pound sign and
end with at least 4 dashes (&lt;code&gt;# Comment ----&lt;/code&gt;), then they will automatically be
organized and appear in an organizational perspective at the bottom left of the Source pane.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/creating-an-rstudio-addin/example-section-break.png&#34; /&gt;&lt;/p&gt;
&lt;p&gt;I’ll take any extra organizational help I can get so I started annotating my code
with comments like these to break it up. Then the idea hit me to create an Addin
and link a keyboard shortcut to it so I can format comments quickly to this standard.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;the-implementation&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;The Implementation&lt;/h2&gt;
&lt;p&gt;The logic is written in R code and the &lt;code&gt;rstudioapi&lt;/code&gt; package is used so that your
code can interact with RStudio. The R code can be complex or really simple (see
the RStudio example on &lt;a href=&#34;https://rstudio.github.io/rstudioaddins/#addin-basics&#34; target=&#34;_blank&#34;&gt;Addin basics&lt;/a&gt;).
However, you must structure your project with certain metadata, like an R package, that
will allow RStudio to automatically discover and register these addins when the
addin is installed.&lt;/p&gt;
&lt;p&gt;The R code should be documented using the &lt;code&gt;roxygen2&lt;/code&gt; style. Here is the small
bit of regex that powers most of my sectioncomment addin.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#&amp;#39; Function to create the padded comment
#&amp;#39; 
#&amp;#39; This function is the logic that runs to reformat
#&amp;#39; the comment as a section break
#&amp;#39; 
#&amp;#39; @usage comment_styler(x, doc_mode=FALSE, l = 80)
#&amp;#39; @importFrom stringr str_pad
#&amp;#39; @param x a string to format
#&amp;#39; @param doc_mode a logical indicating only to convert 
#&amp;#39; lines starting with at least a double hash sign
#&amp;#39; @param l an integer indicating length to pad
#&amp;#39; @return a string that is formatted
comment_styler &amp;lt;- function(x, doc_mode=FALSE, l=80) {
  
  starter_regex &amp;lt;- &amp;#39;(\\s*#+\\s*)(.*)&amp;#39;
  
  if(doc_mode)
    starter_regex &amp;lt;- &amp;#39;(\\s*#{2,}\\s*)(.*)&amp;#39;
  
  if(grepl(starter_regex, x)){
    clean_x &amp;lt;- trimws(gsub(&amp;#39;-*$&amp;#39;, &amp;#39;&amp;#39;, gsub(&amp;#39;-+\\s+-+&amp;#39;, &amp;#39;&amp;#39;, trimws(x))))
    x &amp;lt;- str_pad(gsub(starter_regex, &amp;#39;# \\2 &amp;#39;, clean_x), 
                 width=l, side=&amp;#39;right&amp;#39;, pad=&amp;#39;-&amp;#39;)  
  }
  
  return(x)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;As you can see, it’s not very sophisticated. I just look for text in the Source
pane that appear to be a section comment and pad the line up.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;how-to-use-this-addin&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;How to Use this Addin&lt;/h2&gt;
&lt;p&gt;If you like the idea of my Addin you can install from RStudio with the following
command:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;devtools::install_github(&amp;quot;StevenMMortimer/sectioncomment&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After installing it should apper in your Addins dropdown and you can use it, like so:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/creating-an-rstudio-addin/existing_comment.gif&#34; /&gt;&lt;/p&gt;
&lt;p&gt;My favorite reason to use is that I can write an entire outline of the steps I want
to perform before writing a single line of code, then turn those steps into sections.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://stevenmortimer.com/blog/creating-an-rstudio-addin/comment_selection.gif&#34; /&gt;&lt;/p&gt;
&lt;p&gt;This a great way to force yourself to think about what you are
going to code before diving in head first and getting lost in a jumbled mess of
your own thoughts. I hope you enjoy using it as much as I do and you consider
creating your own addin.&lt;/p&gt;
&lt;/div&gt;

      </description>
    </item>
    
    <item>
      <title>The Unfinished duplicated Function</title>
      <link>https://stevenmortimer.com/the-unfinished-duplicated-function/</link>
      <pubDate>Sun, 20 Nov 2016 00:00:00 +0000</pubDate>
      <author>Steven M. Mortimer</author>
      <guid>https://stevenmortimer.com/the-unfinished-duplicated-function/</guid>
      <description>
        


&lt;p&gt;If you’re a regular R user, then you’ve probably used or have seen the function
&lt;code&gt;duplicated()&lt;/code&gt;. If you’ve only grown up in the world of &lt;code&gt;dplyr&lt;/code&gt; and other
&lt;code&gt;tidyverse&lt;/code&gt; packages with its cool function &lt;code&gt;distinct()&lt;/code&gt;, then I highly envy you. In any
case, you might find yourself using the &lt;code&gt;duplicated()&lt;/code&gt; function when you need
some more control. The function has a handy argument &lt;code&gt;incomparables&lt;/code&gt; that allows
you to ignore certain values when doing the comparison for duplicates. One day I
wanted to use this to remove duplicate customer entries from our CRM system based
on name and phone number. I went through the typical pre-processing stages of removing
unnecessary punctionation, converting to lowercase, removing spaces, etc.
After I got familiar with the data I realized that some of the data points were
missing or not complete enough to make any meaningful determination of a duplicate.
I needed a solution to ignore certain missing patterns, but it needed to encompass
two or more columns of data. One hack would be to &lt;code&gt;paste0()&lt;/code&gt; the columns together and
work on a single pasted column, but this doesn’t handle multiple incomparable values.
As I found out, the duplicated function can handle multiple columns, but the
&lt;code&gt;incomparables&lt;/code&gt; argument does not work for multiple columns, let alone different
incomparable values for each column. Here is the error I get:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;duplicated(mtcars[,c(&amp;#39;am&amp;#39;, &amp;#39;gear&amp;#39;, &amp;#39;cyl&amp;#39;)], incomparables=6)&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## Error: argument &amp;#39;incomparables != FALSE&amp;#39; is not used (yet)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is really a bummer because it is nice to have this explicit functionality of
what should really be considered when evaluating for duplicates. The error message
sure is hopeful at the end there: “is not used (yet)”! After some digging I concluded that
the function existed, as-is, at least since R version 1.4, which was released in 2001!
I wouldn’t hold my breath for anyone to actually fix this issue. Contributing
changes to base R has become much for difficult. I submitted a patch to R core to fix
this very issue, but they weren’t too responsive. I guess it will stay as a hidden gem
in base R, waiting for the next data miner to unearth. If you run across this issue
and really want a fix, then feel free to use the code below.
I’ve implemented a function to support incomparables across multiple columns where
a list of vectors will specify the incomparable values for each column. The list
elements are recycled if it is shorter than the columns being compared.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;new_duplicated &amp;lt;- function(x, incomparables = FALSE, fromLast = FALSE, ...) {
  
 if(!identical(incomparables, FALSE)) {
    n &amp;lt;- ncol(x)
    nmx &amp;lt;- names(x)
    nmincomparables &amp;lt;- names(incomparables)
    lincomparables &amp;lt;- length(incomparables)
    if(is.null(nmincomparables)) {
      if(lincomparables &amp;lt; n) {
        # pad any incomparables lists with the default value if list is shorter than the number columns in the supplied data.frame
        tmp &amp;lt;- c(incomparables, as.list(rep_len(FALSE, n - lincomparables)))
        names(tmp) &amp;lt;- nmx
        incomparables &amp;lt;- tmp 
      }
      if(lincomparables &amp;gt; n) {
        # if the list is unnamed and there are more elements in the list than there are columns, then only first n elements
        warning(paste(&amp;quot;more columns in &amp;#39;incomparables&amp;#39; than x, only using the first&amp;quot;, n, &amp;quot;elements&amp;quot;))
        incomparables &amp;lt;- incomparables[1:n]
      }
    } else {
      # for named lists, find match, else default value
      tmp &amp;lt;- as.list(rep_len(FALSE, n))
      names(tmp) &amp;lt;- nmx
      i &amp;lt;- match(nmincomparables, nmx, 0L)
      if(any(i &amp;lt;= 0L))
        warning(&amp;quot;not all columns named in &amp;#39;incomparables&amp;#39; exist&amp;quot;)
      tmp[ i[i &amp;gt; 0L] ] &amp;lt;- incomparables[i &amp;gt; 0L]
      incomparables &amp;lt;- tmp[nmx]
    }
    
    # first determine duplicates, then override when an incomparable value is found in a row since the existence of even 1 incomparable value in a row means it cannot be a duplicate
    res &amp;lt;- duplicated(do.call(&amp;quot;paste&amp;quot;, c(x, sep=&amp;quot;\r&amp;quot;)), fromLast = fromLast)
    
    #for better performance only bother with the columns that have incomparable values not set to the default: !identical(x, FALSE)
    run_incomp_check &amp;lt;- sapply(incomparables, FUN=function(x){!identical(x, FALSE)})
    if (sum(run_incomp_check) &amp;gt; 0L){
      incomp_check &amp;lt;- mapply(FUN=function(column,incomparables){match(column, incomparables)}, x[run_incomp_check], incomparables[run_incomp_check])
      # any rows with an incomparable match means, TRUE, it can override the duplicated result
      overwrite &amp;lt;- apply(data.frame(incomp_check), 1, function(x){any(!is.na(x))})
      res[overwrite] &amp;lt;- FALSE
    }
    
    return(res)
  } else if(length(x) != 1L) {
    duplicated(do.call(&amp;quot;paste&amp;quot;, c(x, sep=&amp;quot;\r&amp;quot;)), fromLast = fromLast)
  } else {
    duplicated(x[[1L]], fromLast = fromLast, ...)
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the function in action:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;mtcars2 &amp;lt;- head(mtcars[,c(&amp;#39;am&amp;#39;, &amp;#39;gear&amp;#39;, &amp;#39;cyl&amp;#39;)])
mtcars2$dup &amp;lt;- new_duplicated(mtcars2, incomparables=1)
mtcars2&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;##                   am gear cyl   dup
## Mazda RX4          1    4   6 FALSE
## Mazda RX4 Wag      1    4   6 FALSE
## Datsun 710         1    4   4 FALSE
## Hornet 4 Drive     0    3   6 FALSE
## Hornet Sportabout  0    3   8 FALSE
## Valiant            0    3   6  TRUE&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typically, the second row would be considered a duplicate of the first row,
but since 1 is an incomparable value it is not considered a duplicate, in fact,
any row containing a 1 in the column will be labeled as false since it renders the
row incomparable.&lt;/p&gt;

      </description>
    </item>
    
  </channel>
</rss>