Variable importance with EvoTrees.jl

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2023/08/11/evotrees.html

Introduction

Variable importance in predictive modeling context
can have several varied meanings. Today I want to investigate two of them:

  • How important is a feature with respect to a given machine learning model?
  • How important is a feature with respect to a given learning algorithm and a given dataset?

In the post we will discuss why and how these two concepts can differ using an example
of boosted trees. We will use their implementation available in the EvoTrees.jl package.

The post was written under Julia 1.9.2, DataFrames.jl 1.9.2, Distributions.jl 0.25.98,
and EvoTrees.jl 0.15.2.

Generating the test data

Let us start with loading the packages we are going to use and generating the test data.

What I want to have is a data set with 10,000 observations.
We will have 9 continuous features denoted x1 to x9 and a continuous target variable y.
Our goal is to have triplets of features (x1 to x3, x4 to x6, x7 to x9)
highly correlated together (but independent between groups) and having a different
correlation level with the y target. I have made the within-group variables
highly correlated but non-identical (so they are distinguishable and have
a slightly different correlation with y in the sampled data set).

The code generating such data is as follows:

using DataFrames
using Distributions
using EvoTrees
using LinearAlgebra
using Random

δ = 1.0e-6
b = fill(1.0 - δ, 3, 3) + δ * I
z = zeros(3, 3)
y = fill(0.5, 3)
dist = MvNormal([b      z  z      0.8*y
                 z      b  z      y
                 z      z  b      1.2*y
                 0.8*y' y' 1.2*y' 1.0])
Random.seed!(1)
mat = rand(dist, 10_000);
df = DataFrame(transpose(mat), [string.("x", 1:9); "y"]);

Let us have a peek at the data:

julia> names(df)
10-element Vector{String}:
 "x1"
 "x2"
 "x3"
 "x4"
 "x5"
 "x6"
 "x7"
 "x8"
 "x9"
 "y"

julia> df
10000×10 DataFrame
   Row │ x1           x2          x3           x4           x5           x6          ⋯
       │ Float64      Float64     Float64      Float64      Float64      Float64     ⋯
───────┼──────────────────────────────────────────────────────────────────────────────
     1 │  0.0619327    0.0623264   0.0613998    0.0466594    0.0481949    0.0454962  ⋯
     2 │  0.217879     0.216951    0.217742     0.00738607   0.00888615   0.00626926
     3 │  1.54641      1.54598     1.54328     -1.00261     -1.0013      -0.999863
     4 │  0.208777     0.207593    0.209145    -1.21253     -1.21556     -1.21462
     5 │ -0.458805    -0.458081   -0.457956     0.103491     0.10537      0.103313   ⋯
     6 │  0.392072     0.390938    0.390447    -0.354123    -0.354995    -0.353026
     7 │ -0.313095    -0.310223   -0.311185     1.09256      1.09373      1.09443
   ⋮   │      ⋮           ⋮            ⋮            ⋮            ⋮            ⋮      ⋱
  9994 │ -1.24411     -1.24363    -1.24439     -0.789893    -0.793004    -0.792177
  9995 │  0.199036     0.199199    0.199344     0.945945     0.945308     0.943717   ⋯
  9996 │  1.81075      1.80926     1.81064     -2.53813     -2.53805     -2.53996
  9997 │ -0.00896532  -0.0079907  -0.00876527  -0.629303    -0.629402    -0.630129
  9998 │ -1.62881     -1.62626    -1.62703     -0.222873    -0.222469    -0.22166
  9999 │  1.45152      1.44833     1.45131     -2.543       -2.54377     -2.544      ⋯
 10000 │  0.436075     0.435492    0.436974    -0.28131     -0.281519    -0.283039
                                                       4 columns and 9986 rows omitted

Indeed we see that there are 9 features and one target variable. Also we visually
see that variables x1, x2, and x3 are almost the same but not identical.
Similarly x4, x5, and x6.
(I have cropped the rest of the printout as it was too wide for the post.)

I chose such a data generation scheme since a priori, that is
with respect to a given machine learning algorithm and a given dataset,
their importance is as follows:

  • x1, x2, and x3 should have a very similar and lowest importance
    (their correlation with y is lowest by design);
  • x4, x5, and x6 should have a very similar and medium importance;
  • x7, x8, and x9 should have a very similar and highest importance
    (their correlation with y is highest by design).

However, if we build a specific boosted tree model can we expect the same relationship?
Let us check.

Variable importance with respect to a specific model

We build a boosted tree model (using the default settings) and evaluate
variable importance of the features:

julia> model = fit_evotree(EvoTreeRegressor(),
                           df;
                           target_name="y",
                           verbosity=0);

julia> EvoTrees.importance(model)
9-element Vector{Pair{String, Float64}}:
 "x9" => 0.33002820995126636
 "x4" => 0.17950260124468856
 "x5" => 0.10630471720405912
 "x7" => 0.1002898622306779
 "x1" => 0.09023808819243322
 "x8" => 0.060680998291169054
 "x3" => 0.04789330560493748
 "x6" => 0.044689013127277216
 "x2" => 0.040373204153491105

We see that x9 feature has the highest importance, but it is quite different
from x8 and x7. The x4 feature, although it has a lower correlation with
y than e.g. the x8 feature has a higher variable importance (the same holds
for x1 vs x8).

What is the reason for such a situation? When a boosted tree model is built it
seems that what has happened is that x9 variable captured most of the value
of explanation of y from x7 and x8 variables as they are very similar.
Therefore, in this specific model, x7 and x8 are not that important.

Let us try estimating the model for the second time to see if we notice any difference:

julia> model = fit_evotree(EvoTreeRegressor(),
                           df;
                           target_name="y",
                           verbosity=0);

julia> EvoTrees.importance(model)
9-element Vector{Pair{String, Float64}}:
 "x9" => 0.33002820995126636
 "x4" => 0.17950260124468856
 "x5" => 0.10630471720405912
 "x7" => 0.1002898622306779
 "x1" => 0.09023808819243322
 "x8" => 0.060680998291169054
 "x3" => 0.04789330560493748
 "x6" => 0.044689013127277216
 "x2" => 0.040373204153491105

The results are identical. You might wonder what is the reason for this? The cause
of this situation is that the fit_evotree function uses a default seed when doing
computations so we get the same tree twice. To be precise, when we call
EvoTreeRegressor() it sets the seed of the default random number generator in the
current task to 123.

So let us try shuffling the variables to see if we would get a different result:

julia> model = fit_evotree(EvoTreeRegressor(),
                           df[!, randperm(10)];
                           target_name="y",
                           verbosity=0);

julia> EvoTrees.importance(model)
9-element Vector{Pair{String, Float64}}:
 "x9" => 0.23187113718728977
 "x8" => 0.20285271199278873
 "x4" => 0.16779901582722756
 "x5" => 0.15415181545562057
 "x3" => 0.08494533205347177
 "x2" => 0.06781415123236784
 "x7" => 0.05788796227269619
 "x1" => 0.024214826049282448
 "x6" => 0.008463047929255035

Indeed the variable importance changed. Would it be still different if we did another
randomized run?

julia> model = fit_evotree(EvoTreeRegressor(),
                           df[!, randperm(10)];
                           target_name="y",
                           verbosity=0);

julia> EvoTrees.importance(model)
9-element Vector{Pair{String, Float64}}:
 "x9" => 0.23187113718728977
 "x8" => 0.20285271199278873
 "x4" => 0.16779901582722756
 "x5" => 0.15415181545562057
 "x3" => 0.08494533205347177
 "x2" => 0.06781415123236784
 "x7" => 0.05788796227269619
 "x1" => 0.024214826049282448
 "x6" => 0.008463047929255035

Maybe this was a surprise to you but the answer is: no. We get the same results.
What is going on?

This time the answer is that the fit_evotree and randperm functions share the same
random number generator (as we run them in the same task) and fit_evotree resets its state
to 123 when we call EvoTreeRegressor().
This means that when we invoked randperm the generator was in the same state both times so the
randperm(10) call produced the same sequence of numbers.

Variable importance with respect to an algorithm and a data set

We were given an important general lesson. We need to properly initialize the functions
that use randomness in our code. Let us leverage this knowledge to
try assessing variable importance with respect to a boosted trees and a data set
we have generated.

What I do in the code below is generating 1000 boosted trees
and computing mean variable importance (along with some additional statistics)
across all of them:

julia> rng = Xoshiro(1);

julia> reduce(vcat,
           map(1:1000) do _
               fit_evotree(EvoTreeRegressor(rng=rng),
                           df;
                           target_name="y",
                           verbosity=0) |>
               EvoTrees.importance |>
               sort |>
               DataFrame
           end) |> describe
9×7 DataFrame
 Row │ variable  mean       min         median     max        nmissing  eltype
     │ Symbol    Float64    Float64     Float64    Float64    Int64     DataType
─────┼───────────────────────────────────────────────────────────────────────────
   1 │ x1        0.094902   0.0456523   0.0946136  0.1437            0  Float64
   2 │ x2        0.0475577  0.0109109   0.0469086  0.0969166         0  Float64
   3 │ x3        0.0361729  0.00189622  0.0353323  0.0844639         0  Float64
   4 │ x4        0.128568   0.032614    0.127093   0.285664          0  Float64
   5 │ x5        0.111577   0.00310871  0.109548   0.238868          0  Float64
   6 │ x6        0.0891413  0.0         0.0879858  0.196108          0  Float64
   7 │ x7        0.189893   0.0400903   0.179193   0.417476          0  Float64
   8 │ x8        0.155352   0.013377    0.154884   0.371452          0  Float64
   9 │ x9        0.146837   0.00642872  0.141293   0.397151          0  Float64

This time we see a better separation between variables x1, x2, and x3, followed
by x4, x5, and x6, and finally the x7, x8, and x9 group.
However, still we see some non-negligible within-group differences (and x1 is even better than x6).
It seems that just ensuring that we properly pass the random number generator
the fit_evotree model random number generator is not enough.

Let us then do a final attempt. This time we both properly pass the random number generator to
the fit_evotree model and randomize the order of variables in the source data frame
(making sure we also properly pass the random number generator to the randperm function):

julia> reduce(vcat,
           map(1:1000) do _
               fit_evotree(EvoTreeRegressor(rng=rng),
                           df[!, randperm(rng, 10)];
                           target_name="y",
                           verbosity=0) |>
               EvoTrees.importance |>
               sort |>
               DataFrame
           end) |> describe
9×7 DataFrame
 Row │ variable  mean       min         median     max       nmissing  eltype
     │ Symbol    Float64    Float64     Float64    Float64   Int64     DataType
─────┼──────────────────────────────────────────────────────────────────────────
   1 │ x1        0.0595918  0.00260002  0.0524585  0.153613         0  Float64
   2 │ x2        0.0604078  0.00374867  0.0534607  0.148399         0  Float64
   3 │ x3        0.0586352  0.00219417  0.051398   0.140393         0  Float64
   4 │ x4        0.103219   0.00484802  0.101625   0.252127         0  Float64
   5 │ x5        0.119415   0.00146451  0.116338   0.268475         0  Float64
   6 │ x6        0.106432   0.00672514  0.102708   0.254382         0  Float64
   7 │ x7        0.153185   0.00346922  0.139289   0.388995         0  Float64
   8 │ x8        0.166709   0.00651302  0.161872   0.419284         0  Float64
   9 │ x9        0.172406   0.00979053  0.166798   0.444192         0  Float64

This time we have succeeded. While there is still some variability in within-group
variable importance it is small, and the groups are clearly separated. The worst
features have their importance around 6%, the medium value features around 11%,
and the best features around 16%.

Conclusions

Let us recap what we have seen today.

First, we see that variable importance with respect to a given concrete instance
of a machine learning model can be significantly different from variable importance
with respect to a given learning algorithm and a given dataset. Therefore, one should
carefully think which one is interesting from the perspective of a problem that one
wants to solve.

The second lesson is related to implementation details of
machine learning algorithms:

  1. Many of them use pseudorandom numbers when building a model and proper
    handling of pseudorandom generator is crucial.
  2. The result of model building can depend on the order of features in the
    source data set. In our example we have seen that shuffling the columns
    of an input data table produced significantly different variable
    importance results.

I hope you found these examples useful!

Creating your own blog with Julia and Franklin

By: Navi

Re-posted from: https://indymnv.dev/posts/006_build_blog/index.html

Creating your own blog with Julia and Franklin

Date: 2023-08-10

Summary: Describing the steps to create your own blog, so you can stop posting your code on Tinder

tags: #Julia #Writing #WebDevelopment #Franklin






Table of contents

  1. Introduction
  2. Some Reasons to Create Your Own Blog
  3. Installation
  4. First Steps
    1. Selecting a template
    2. Cleaning the template
    3. Creating your first post
  5. Deployment
    1. Hosting in a different domain (optional)
  6. RSS and Tags
    1. Host your Feed to JuliaBloggers (optional)
  7. Conclusions
  8. Acknowledgment

Introduction

In this post, we are going to discuss how to build your own blog with Julia and Franklin.jl, a popular static site generator among Julia users who create their own blogs or even build websites for tutorials. I hope that if you are reading this entry and you don't have your own space, it can motivate you to build your own website.

Some Reasons to Create Your Own Blog

Blogs may sound old-fashioned, something created by people who are still living in the 90s, typing with passion about the political system while listening to Soundgarden in the background and drinking some kind of cheap beer… or programmers. And because if you are reading this content, you're probably at least the second one, you should consider that having a blog is a nice way to:

  • Track your progress in your field

  • Generate content that can be useful for somebody else

  • Help the open-source community with diffusion, tutorials, etc.

  • Create your own space and adapt it you your needs

  • Build your personal brand and help you to find a job

But why Franklin? Franklin is one of the most popular libraries for this purpose in Julia. It offers seamless integration with running Julia scripts so you can use julia for demostrations in your blog this coud be harder with other static site generators. If you only want to create basic entries with some code and images, perhaps Franklin.jl might not be that different from Hugo or Jekyll.

Installation

The first step is to create a folder where you will save your project. Once you are ready, open the Julia REPL in the location where the folder should be. When it's ready, type ] to activate the package manager and then type:

(@v1.9) pkg> add Franklin

then, return to the Julia Repl and import the library:

julia> using Franklin

Remember to make sure you have successfully installed the Franklin library before trying to import it.

First Steps

To create your website, you can choose one of the templates available. In my case, I just used the basic one, but if you have a different preference, feel free to go ahead; they all follow similar structures. You can also import another template that you like more and adapt it to your website. Please read the documentation for instructions on how to do this.

Selecting a template

Once you have decided your template, type in the REPL the next instruction

julia> newsite("myBlog", template="basic") #you can choose another name and template

This will create a folder with various directories and elements. It will also activate the environment inside the project. So, if you verify the project with ], it should display the name of your project.

.
├── 404.md            # Page for error 404
├── Manifest.toml     # The typical toml files for Julia development project
├── Project.toml
├── __site            # Generate your full website.
├── _assets           # You can add pictures and images here
├── _css              # All related to styling your website
├── _layout           # All related to the structure of your website
├── _libs             # Here will go all elements for website like katex, searchbar, etc  
├── _rss              # A couple of files related to rss feed, 
├── config.md         # Set Global variables for your website
├── index.md          # Main landing page
├── pages.md          # All your pages / you can create your folder or organize in different way
└── utils.jl          # Julia File for setting some configurations

Finally type:

julia> serve()

It should open your website locally in the browser, and it should look exactly the same as the template website you chose.

starting template

From this point, it's time to delete some files and content. You might also want to add some pages for your projects, about, contact, etc. This is up to you, but for now, we are going to keep just 2 pages: one for the main "about" page and another to host all your posts.

Cleaning the template

Now, go to the "index.md" page and delete all its content. This page will become your main page, and you can mix HTML and Markdown in this file to add whatever you want to it.

# Welcome to my blog
## I am using Franklin~~~
    <img src="/assets/rndimg.jpg" height="300" class="main-picture" >
    <p>
    <p>
~~~
This is an introductory message

You might have noticed that in our main page, there are four links to different pages. You can choose to keep those links or delete them all. However, for the purpose of creating a blog section, let's use one of those links. To do that, follow these steps:

  1. Go to the "header.html" file located in the "layout" folder.

  2. Modify the code in the "header.html" file to something like this:

<header>
<div class="blog-name"><a href="/"></a>Amazing Blog</div>
<nav>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/menu1/">Blog</a></li>
  </ul>
  <img src="/assets/hamburger.svg" id="menu-icon">
</nav>
</header>

If you're looking to change the background color to something more interesting than white, now is the time to showcase your frontend skills. Follow these steps:

  1. Navigate to the "franklin.css" file.

  2. In the first block of code, add the background color that you prefer. For instance:

:root {
  --block-background: hsl(0, 0%, 94%);
  --output-background: hsl(0, 0%, 98%);
  --small: 14px;
  --normal: 19px;
  --text-color: hsv(0, 0%, 20%);
	background-color: aqua;
}

Finally, after making these modifications, the result should look something like this:

frontend

Creating your first post

Now, if you're ready to start your own blog, here's how you can set up the "posts" folder to add your articles, create a new folder named "posts" in the same root directory as your other folders. Is important to consider this things.

  • Inside the "posts" folder, you can add all your articles. You have the flexibility to use both Markdown files and HTML files for your articles.

  • If you're doing literate programming with tools like Pluto or Jupyter, you can export your notebooks to HTML format and place them in the "posts" folder. This way, anyone can easily view your data science projects.

For now, let's add a file called test1.md inside the posts folder and you can add some text

# This is a title in my first post
 
So I can write anything## Here is an introductionWe are going to write some code:using LinearAlgebra
a = [1, 2, 3, 3, 4, 5, 2, 2]
@show dot(a, a)
println(dot(a, a))

Then, go to the menu1.md file, erase the remaining content, and create a link to the test1.md file. This is as simple as:

* [This is the title of my blog](../posts/test1)

If you save it, and navigate to http://localhost:8000/posts/test1/, you should see your post displayed clearly. This page will include your "about" section and the space to write your blog content. Congratulations! You now have a basic understanding of how Franklin works and can make any further edits or modifications you desire.

If you wish to further style your website, please go ahead and customize it to your heart's content.

Deployment

Now it's time to host your website in some place. One of the most straightforward options is using GitHub. Here's how you can do it:

  1. Create a Repository: Go to your GitHub account and create an empty repository. When entering the name of your project, you have two paths to choose from:

    a. If this is a personal website or organization, the name of your project should be something like username.github.io.

    b. You can create your own custom name for your project, like myblog.

If you're unsure which option to choose, I recommend going with option (a) because it's more straightforward. If you choose option (b), you'll need to define a prepath variable in your config.md with the name of that project. For instance: @def prepath = "myblog".

  1. Upload Your Project: Now upload your project to GitHub, following the instructions in your repository.

  2. Configure GitHub Pages: Once you've pushed your project, go to the Settings tab in your repository. Then navigate to GitHub Pages. In the Source dropdown, select gh-pages. If you see a message indicating success, your project is now live.

  3. Check Your Website: You can now open your web browser and enter the link of your project, which would be username.github.io. If you can see your website, congratulations! Your blog is now live on the internet.

By following these steps, you've successfully hosted your Franklin-generated website on GitHub Pages. It's now accessible to anyone with the link, and you can share your content with the world.

Hosting in a different domain (optional)

If you're hesitant to share your GitHub username due to its lengthy or unconventional extension, or if you prefer a more professional-looking link, you might want to consider an alternative domain, such as .com or .dev. You can purchase a domain and link it to your website. For example, you can use services like Google Domains to find and purchase a domain that suits your preference.

Once you've found and acquired the domain you like, you can proceed to link it to your website. To do this, you need to configure the DNS settings. You can find detailed explanations about custom domains and GitHub Pages in the documentation. In a nutshell, follow these steps:

  1. Go to Google Domains, select your domain, and navigate to the DNS section.

  2. Configure the DNS records, as shown below:

dns_setup

  1. After correctly setting up the DNS records, go to your GitHub project repository's settings, then navigate to Pages and enter your custom domain:

custom_domain

  1. If everything is set up correctly, GitHub will confirm the configuration. In a few minutes, your website should become accessible via your new custom domain.

By following these steps, you'll be able to link a custom domain to your Franklin-generated website, providing a more personalized and professional web presence.

RSS and Tags

Now that your website is up and running, setting up an RSS feed is important for people who want to stay updated on your new articles without having to visit your website daily. Tools like Newsboat or Inoreader help users keep track of updates from various websites, making an RSS feed a valuable addition to your blog.

Thankfully, Franklin makes setting up an RSS feed quite simple. All you need to do is go to each page in your "posts" folder and add a small description within +++ brackets, like this:

+++
tags = ["Julia", "Writing"] rss_title = "Creating your own blog with Julia and Franklin"
rss_description = "Describing the steps to create your own blog, so you can stop posting your code on Instagram"
rss_pubdate = Date(2023, 8, 10) 
+++

The RSS fields you add will be included in the information extracted by platforms like Newsboat. From these applications, I can read the title, a brief description, and the publication date and all the content if it's available. Additionally, you'll notice a "tags" section. This is also important because it allows users to filter by topics. For example, if you write different blogs about topics ranging from Julia programming to analysis of Shakira's new songs, users can select the topics they're specifically interested in.

To share your blog's RSS feed, you'll need a URL like https://www.yourdomain.com/feed.xml. Make sure to prominently display this URL in your website so that readers can easily find and subscribe to your feed.

Host your Feed to JuliaBloggers (optional)

Lastly, if you're considering writing about Julia and want to contribute to the community, don't hesitate to share your work. Whether it's a calculator project, a website, a 2D game, or a cutting-edge machine learning algorithm, your contributions will help the Julia community grow and provide valuable insights for others to learn from.

Visit the JuliaBloggers Website and add your information. In the "Feed URL" field, you can use a URL similar to the first example you mentioned, like:

  • http://indymnv.dev/tag/julia/feed/

Once you've submitted this information, every time you publish a new post on your website, the community will be able to see it. If you want to test this process first, you can use an RSS reader like Newsboat or Inoreader to ensure that your updates are being picked up as expected.

Conclusions

I hope you enjoyed reading this article. If you haven't yet created your own website, I hope it serves as motivation to get started, whether you choose to use Franklin or another static site generator. Having your own online space to write about your interests and dive as deep as you like is a rewarding endeavor. Don't hesitate to embark on this journey and create a platform that showcases your passion and expertise. Happy blogging!

Acknowledgment

I also want to thank Thibaut Lienart, who is the main developer of Franklin. His work has been incredibly beneficial for the community.

Argument Matching Across Languages

By: Jonathan Carroll

Re-posted from: https://jcarroll.com.au/2023/08/06/argument-matching-across-languages/

With Functional Programming, we write functions which take arguments and do something with
or based on those arguments. You might not think there’s much to learn about given that
tiny description of “an argument to a function” but the syntax and mechanics of different
languages is actually widely variable and intricate.

Let’s say I have some function in R that takes three arguments, x, y, and z,
and just prints them out in a string in that order.

r_fun <- function(x, y, z) {
  sprintf("arguments are: %s, %s, %s", x, y, z)
}

Calling this function with good practices (specifying all the argument names in full)
would look like this

r_fun(x = "a", y = "b", z = "c")
## [1] "arguments are: a, b, c"

I said “in full” because by default, R will happily do partial matching, so long
as it can uniquely figure out which argument you mean

long_args <- function(alphabet = "a to z", altitude = 100) {
  print(sprintf("alphabet: %s", alphabet))
  print(sprintf("altitude: %d", altitude))
}
long_args(alphabet = "[A-Z]", altitude = 50)
## [1] "alphabet: [A-Z]"
## [1] "altitude: 50"

In this case, both arguments start with "al" so it’s ambiguous up to there

long_args(al = "letters")
## Error in long_args(al = "letters"): argument 1 matches multiple formal arguments

but we only need to specify enough letters to disambiguate

long_args(alpha = "LETTERS", alt = 200)
## [1] "alphabet: LETTERS"
## [1] "altitude: 200"

Relying on this behaviour is dangerous, and it’s recommended to turn on warnings
when this happens with

options(warnPartialMatchArgs = TRUE)
long_args(alpha = "LETTERS", alt = 200)
## Warning in long_args(alpha = "LETTERS", alt = 200): partial argument match of
## 'alpha' to 'alphabet'
## Warning in long_args(alpha = "LETTERS", alt = 200): partial argument match of
## 'alt' to 'altitude'
## [1] "alphabet: LETTERS"
## [1] "altitude: 200"

You don’t have to use argument names when calling the function, though – you can just rely on positional arguments

r_fun("a", "b", "c")
## [1] "arguments are: a, b, c"

and this is very commonly done, despite it being less clear to what any of those
refer, and runs the risk that the function changes argument ordering in an updated
version. It works, though.

Extensive sidenote: square-bracket matrix subsetting officially uses the (poorly? traditionally?)
named arguments i and j as [i, j] but it actually entirely ignores them and uses
positional arguments. The documentation (?`[`) does warn about this

“Note that these operations do not match their index arguments in the standard way:
argument names are ignored and positional matching only is used. So m[j = 2, i = 1] is
equivalent to m[2, 1] and not to m[1, 2].”

but it would be very easy to get bitten by it if one tried to use the names directly

m <- matrix(1:9, 3, 3, byrow = TRUE)
m
##      [,1] [,2] [,3]
## [1,]    1    2    3
## [2,]    4    5    6
## [3,]    7    8    9
m[i = 1, j = 2]
## [1] 2
m[j = 2, i = 1]
## [1] 4

Thomas Lumley
notes that

“it used to be that no primitive functions did argument matching by name.”/” and “-’
and switch() and some others still don’t. I’m not sure why”[” wasn’t changed in 2.11
when a bunch of primitives got normal argument matching.”

Worse still, perhaps – the seq() function creates a sequence of values. It has the
formal arguments with defaults from = 1 and to = 1 so you can calculate

seq(from = 2, to = 5)
## [1] 2 3 4 5

or you can leverage the default of from = 1

seq(to = 5)
## [1] 1 2 3 4 5

However, there are five “forms” in which
you can provide arguments to this function and they behave differently. If you only
specify the first argument unnamed, it treats this as to despite the first argument being from

seq(5)
## [1] 1 2 3 4 5

which is extra strange, because if you do specify to with its ostensibly default value 1, the sequence is backwards

seq(5, to = 1)
## [1] 5 4 3 2 1

Back to our function – a feature that makes R really neat is that you can specify
the named arguments in any order

r_fun(z = "c", x = "a", y = "b")
## [1] "arguments are: a, b, c"

If you don’t specify them by name, R will default to positions, so specifying just
one (e.g. z) but leaving the rest unspecified, R will presume you want the others
in positional order

r_fun(z = "c", "a", "b")
## [1] "arguments are: a, b, c"

Where it gets really interesting is you can go back to named arguments further along
and again, R will figure out that you mean the remaining unnamed argument

r_fun(z = "c", "b", x = "a")
## [1] "arguments are: a, b, c"

This only holds if the function doesn’t use the ellipses ... which
captures “any other arguments” when calling the function, often to be passed
on to another function. If the function signature has ... then all the
unnamed arguments are captured. This example function just
combines any other arguments into a comma-separated string, if there
are any (tested with the under-documented ...length() which returns the number
of arguments captured via ...)

dot_f <- function(a = 1, b = 2, ...) {
  print(sprintf("named arguments: %s, %s", a, b))
  if (...length()) {
    print(sprintf("additional arguments: %s", toString(list(...))))
  }
}

You can call this with just the named arguments

dot_f(a = 3, b = 4)
## [1] "named arguments: 3, 4"

or you can add more argument (no name required)

dot_f(a = 3, b = 4, 5)
## [1] "named arguments: 3, 4"
## [1] "additional arguments: 5"

As before, none of the names are really required, and we can add as
many as we want

dot_f(3, 4, 5, 6, 7)
## [1] "named arguments: 3, 4"
## [1] "additional arguments: 5, 6, 7"

We can name them if we want

dot_f(a = 3, b = 4, blah = 5)
## [1] "named arguments: 3, 4"
## [1] "additional arguments: 5"

but here be danger, because those names can be anything and aren’t matched
to the actual function, so this works (say, I misspelled an argument name a as A)

dot_f(A = 3, B = 4, 5)
## [1] "named arguments: 5, 2"
## [1] "additional arguments: 3, 4"

Notice that the additional arguments are the ones I named (not those in
the function definition); the 5 has been positionally matched to a; and b
has taken its default value of 2 because no other arguments were provided.

We can still mix up the ordering of positions, provided everything else matches up

dot_f(3, b = 4, 5)
## [1] "named arguments: 3, 4"
## [1] "additional arguments: 5"
dot_f(3, b = 4, 5, a = 2)
## [1] "named arguments: 2, 4"
## [1] "additional arguments: 3, 5"

The flexibility in all of this is what encouraged Joe Cheng to use R as an
interface to HTML in the form of shiny, what he calls
“a bizzarely good host language” (should link
to the right timestamp) and he notes that other languages don’t let you do
this sort of mixing up of named and positional arguments.

Okay, that’s R – weird and fun, but a lot of flexibility.

I saw this post mentioned in the #rust hashtag on Mastodon and had a look – it surprised me
at first because I thought “what do you mean Rust doesn’t have named arguments?”…

I’ve become so used to the inline help from VSCode when I’m writing Rust that I
didn’t realise I wasn’t using named arguments.

Here’s a function I wrote for my toy rock-paper-scissors game in Rust

fn play(a: Throw, b: Throw) -> GameResult {
    let result = match a.cmp(&b) {
        Ordering::Equal => GameResult::Tie,
        Ordering::Greater => GameResult::YouWin,
        Ordering::Less => GameResult::YouLose,
    };

    println!("{} {}", "Result:".purple().bold(), result);

    result
}

It has arguments a and b because I did a terrible job naming them – I knew
exactly how I planned to use them, so bad luck to anyone else.

Calling that function further down in the code I have

let user = val.user();
let computer = Throw::computer();
play(user, computer);

BUT what I see in the editor has the argument names, unless I switch off
hints (which I have bound to holding Ctrl+Alt at the moment)

Toggling inlay hints in VSCode

Toggling inlay hints in VSCode

So, I can’t just rearrange arguments in Rust?

If I define a function with two arguments

>> fn two_args(a: f64, b: &str) -> String {
        let res = format!("all arguments: {a}, {b}");
        res
}

then I can call it

>> two_args(42.0, "forty-two")
"all arguments: 42, forty-two"

Just swapping the arguments obviously fails because 42.0 isn’t a &str and
"forty-two" isn’t a f64. But there isn’t a way to say “the value for that
argument is this”; I can’t use any of these

two_args(a = 42.0, b = "forty-two")
two_args(a: 42.0, b: "forty-two")

two_args(b = "forty-two", a = 42.0)
two_args(b: "forty-two", a: 42.0)

I suspect the fact that this was a surprise to me means I’m earlier in my Rust
learning than I had thought – I clearly haven’t built anything that has
functionality I didn’t directly need, because I haven’t had to worry about
calling functions in strange ways yet.

There is one loophole… time to break out another cool toy: {rextendr}

library(rextendr)

rust_function(
  'fn two_args(a: f64, b: &str) -> String {
          let res = format!("all arguments: {a}, {b}");
          res
  }'
)

This produces an R function that takes two arguments, a and b which I can call
as if it was an R function

two_args(a = 42, b = "forty-two")
## [1] "all arguments: 42, forty-two"

I can call it without argument names

two_args(42, "forty-two")
## [1] "all arguments: 42, forty-two"

and I can swap them

two_args(b = "forty-two", a = 42)
## [1] "all arguments: 42, forty-two"

This is just because the argument matching happens before the values get
sent down to the Rust code – the function here is an R function that calls
other code internally

two_args
## function (a, b) 
## .Call("wrap__two_args", a, b, PACKAGE = "librextendr1")
## <bytecode: 0x55d873cff7b8>

I somewhat started out the idea for this blogpost as I was learning some Typescript and
came across this https://github.com/gibbok/typescript-book#typescript-fundamental-comparison-rules

“Function parameters are compared by types, not by their names:”

type X = (a: number) => void;
type Y = (a: number) => void;
let x: X = (j: number) => undefined;
let y: Y = (k: number) => undefined;
y = x; // Valid
x = y; // Valid

which initially struck me as strange, and I needed to work through some examples in a live
setting. On reflection, I think I see that this is exactly what I would specify in
e.g. Haskell – “a function that takes a number”, not “a function with an argument named a which
is a number”

x :: Float -> Nothing

Because technically all functions in Haskell actually only take a single argument (the notation Int -> Int -> Int reveals this
fact nicely, but in practice the notation makes it feel like multiple arguments can be used)
there is no way to “pass arguments by name” but there is a neat way to swap the order
of arguments that a function expects to receive; flip

flip :: (a -> b -> c) -> b -> a -> c

>>> flip (++) "hello" "world"
"worldhello"

-- or

>>> "hello" ++ "world"
"helloworld

Those of you familiar with R’s S3 dispatch functionality will perhaps note that
the ‘first’ argument has a special role; it controls exactly which method will
be called. If we had some function which was flexible in the sense that it could
take several different ‘classes’ and do something different with them, we would
write that as

flexi <- function(a, b) {
  UseMethod("flexi")
}

flexi.matrix <- function(a, b) {
  paste0("a is a matrix, b = ", b)
}

flexi.data.frame <- function(a, b) {
  paste0("a is a data.frame, b = ", b)
}

flexi.default <- function(a, b) {
  paste0("a is something else, b = ", b)
}

Now, depending on whether a is a matrix, a data.frame, or something else, one
of the ‘methods’ will be called

flexi(a = matrix(), b = 7)
## [1] "a is a matrix, b = 7"
flexi(a = data.frame(), b = 8)
## [1] "a is a data.frame, b = 8"
flexi(a = 1, b = 9)
## [1] "a is something else, b = 9"

even if we swap the order of the arguments in the call

flexi(b = 3, a = matrix())
## [1] "a is a matrix, b = 3"

S4 dispatch goes even further and dispatches based on more than just the class of
the first argument. Stuart Lee has a great guide on S4. The point is, you can do something
different depending on what you pass to multiple arguments

s4flexi(matrix(), data.frame(), 7)
s4flexi(matrix(), data.frame(), list())
s4flexi(matrix(), data.frame(), NULL)

Julia has some of the most interesting argument parsing. I love the Haskell-like
function declarations – so little boilerplate! We define some function f that
takes two arguments

f(a, b) = a + b
## f (generic function with 1 method)
f(4, 5)
## 9

Similar to the Rust situation, though – these aren’t named outside of the function body,
so we can’t refer to them either in that order or reversed

f(a = 4, b = 5)
MethodError: no method matching f(; a=4, b=5)
Closest candidates are:
  f(!Matched::Any, !Matched::Any) at none:3 got unsupported keyword arguments "a", "b"

The reason is that Julia uses the python-esque keyword argument syntax, where unnamed
arguments appear first, followed by any keyword arguments following a ;, so we can specify
these correctly as

f(; a, b) = a + b
## f (generic function with 2 methods)
f(a = 4, b = 6)
## 10

Julia is optionally typed, which means we can be flippant with the types here, or we
can be very specific – we can specify that a should be an integer and b should be
a string, and that produces a different method compared to what we already defined. In
this case, I want to return a string with the two values

f(; a::Int, b::String) = "$a; $b"
## f (generic function with 2 methods)
f(a = 42, b = "life, universe, everything")
## "42; life, universe, everything"

Since these are now named, we can swap them

f(b = "L, U, E", a = 42)
## "42; L, U, E"

but what’s even more powerful is we can define a general method, and add type-specific methods
for whatever combination of argument types we want; the first of these returns an integer,
while the other two return strings

g(a, b) = a + b
## g (generic function with 1 method)
g(a::Int, b::String) = "unnamed int, string: $a; $b"
## g (generic function with 2 methods)
g(a::String, b::Int) = "unnamed string, int: $a; $b"
## g (generic function with 3 methods)

Then, depending on what types we provide in each argument, a different method is called

g(3, 2)
## 5
g("abc", 123)
## "unnamed string, int: abc; 123"
g(123, "abc")
## "unnamed int, string: 123; abc"

Similar to S4, but so easy to declare and use! Of course, this doesn’t work if we want these
to be named since that would be ambiguous.

As I’m slowly learning APL, I’ve found it interesting that there’s a well-known approach of
writing “point-free” (“tacit”) functions which don’t specify arguments at all.

Last of all, I’ve had the pleasure of dealing with C this week including passing a pointer
to some object into a function, in which case the value outside of the function is updated.
That’s a whole other post I’m working on.

How does your favourite language use arguments? Let me know! I can be found on Mastodon or use the comments below.

devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.1.2 (2021-11-01)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  tz       Australia/Adelaide
##  date     2023-08-06
##  pandoc   3.1.1 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  assertthat    0.2.1   2019-03-21 [3] CRAN (R 4.0.1)
##  blogdown      1.17    2023-05-16 [1] CRAN (R 4.1.2)
##  bookdown      0.29    2022-09-12 [1] CRAN (R 4.1.2)
##  brio          1.1.3   2021-11-30 [1] CRAN (R 4.1.2)
##  bslib         0.4.1   2022-11-02 [3] CRAN (R 4.2.2)
##  cachem        1.0.6   2021-08-19 [3] CRAN (R 4.2.0)
##  callr         3.7.3   2022-11-02 [3] CRAN (R 4.2.2)
##  cli           3.4.1   2022-09-23 [3] CRAN (R 4.2.1)
##  crayon        1.5.2   2022-09-29 [3] CRAN (R 4.2.1)
##  DBI           1.1.3   2022-06-18 [3] CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 [1] CRAN (R 4.1.2)
##  digest        0.6.30  2022-10-18 [3] CRAN (R 4.2.1)
##  dplyr         1.0.10  2022-09-01 [3] CRAN (R 4.2.1)
##  ellipsis      0.3.2   2021-04-29 [3] CRAN (R 4.1.1)
##  evaluate      0.18    2022-11-07 [3] CRAN (R 4.2.2)
##  fansi         1.0.3   2022-03-24 [3] CRAN (R 4.2.0)
##  fastmap       1.1.0   2021-01-25 [3] CRAN (R 4.2.0)
##  fs            1.5.2   2021-12-08 [3] CRAN (R 4.1.2)
##  generics      0.1.3   2022-07-05 [3] CRAN (R 4.2.1)
##  glue          1.6.2   2022-02-24 [3] CRAN (R 4.2.0)
##  htmltools     0.5.3   2022-07-18 [3] CRAN (R 4.2.1)
##  htmlwidgets   1.5.4   2021-09-08 [1] CRAN (R 4.1.2)
##  httpuv        1.6.6   2022-09-08 [1] CRAN (R 4.1.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.1.2)
##  jsonlite      1.8.3   2022-10-21 [3] CRAN (R 4.2.1)
##  JuliaCall     0.17.5  2022-09-08 [1] CRAN (R 4.1.2)
##  knitr         1.40    2022-08-24 [3] CRAN (R 4.2.1)
##  later         1.3.0   2021-08-18 [1] CRAN (R 4.1.2)
##  lifecycle     1.0.3   2022-10-07 [3] CRAN (R 4.2.1)
##  magrittr      2.0.3   2022-03-30 [3] CRAN (R 4.2.0)
##  memoise       2.0.1   2021-11-26 [3] CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 [3] CRAN (R 4.2.0)
##  miniUI        0.1.1.1 2018-05-18 [1] CRAN (R 4.1.2)
##  pillar        1.8.1   2022-08-19 [3] CRAN (R 4.2.1)
##  pkgbuild      1.4.0   2022-11-27 [1] CRAN (R 4.1.2)
##  pkgconfig     2.0.3   2019-09-22 [3] CRAN (R 4.0.1)
##  pkgload       1.3.0   2022-06-27 [1] CRAN (R 4.1.2)
##  prettyunits   1.1.1   2020-01-24 [3] CRAN (R 4.0.1)
##  processx      3.8.0   2022-10-26 [3] CRAN (R 4.2.1)
##  profvis       0.3.7   2020-11-02 [1] CRAN (R 4.1.2)
##  promises      1.2.0.1 2021-02-11 [1] CRAN (R 4.1.2)
##  ps            1.7.2   2022-10-26 [3] CRAN (R 4.2.2)
##  purrr         1.0.1   2023-01-10 [1] CRAN (R 4.1.2)
##  R6            2.5.1   2021-08-19 [3] CRAN (R 4.2.0)
##  Rcpp          1.0.9   2022-07-08 [1] CRAN (R 4.1.2)
##  remotes       2.4.2   2021-11-30 [1] CRAN (R 4.1.2)
##  rextendr    * 0.3.0   2023-05-30 [1] CRAN (R 4.1.2)
##  rlang         1.0.6   2022-09-24 [1] CRAN (R 4.1.2)
##  rmarkdown     2.18    2022-11-09 [3] CRAN (R 4.2.2)
##  rprojroot     2.0.3   2022-04-02 [1] CRAN (R 4.1.2)
##  rstudioapi    0.14    2022-08-22 [3] CRAN (R 4.2.1)
##  sass          0.4.2   2022-07-16 [3] CRAN (R 4.2.1)
##  sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.1.2)
##  shiny         1.7.2   2022-07-19 [1] CRAN (R 4.1.2)
##  stringi       1.7.8   2022-07-11 [3] CRAN (R 4.2.1)
##  stringr       1.5.0   2022-12-02 [1] CRAN (R 4.1.2)
##  tibble        3.1.8   2022-07-22 [3] CRAN (R 4.2.2)
##  tidyselect    1.2.0   2022-10-10 [3] CRAN (R 4.2.1)
##  urlchecker    1.0.1   2021-11-30 [1] CRAN (R 4.1.2)
##  usethis       2.1.6   2022-05-25 [1] CRAN (R 4.1.2)
##  utf8          1.2.2   2021-07-24 [3] CRAN (R 4.2.0)
##  vctrs         0.5.2   2023-01-23 [1] CRAN (R 4.1.2)
##  withr         2.5.0   2022-03-03 [3] CRAN (R 4.2.0)
##  xfun          0.34    2022-10-18 [3] CRAN (R 4.2.1)
##  xtable        1.8-4   2019-04-21 [1] CRAN (R 4.1.2)
##  yaml          2.3.6   2022-10-18 [3] CRAN (R 4.2.1)
## 
##  [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.1
##  [2] /usr/local/lib/R/site-library
##  [3] /usr/lib/R/site-library
##  [4] /usr/lib/R/library
## 
## ──────────────────────────────────────────────────────────────────────────────