<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Algorithm | Algorist</title><link>https://www.algorist.co.uk/tag/algorithm/</link><atom:link href="https://www.algorist.co.uk/tag/algorithm/index.xml" rel="self" type="application/rss+xml"/><description>Algorithm</description><generator>Wowchemy (https://wowchemy.com)</generator><language>en-gb</language><lastBuildDate>Fri, 18 Dec 2020 00:00:00 +0000</lastBuildDate><image><url>https://www.algorist.co.uk/images/icon_hu1a112552fd764c0568141c667be1573d_16776_512x512_fill_lanczos_center_2.png</url><title>Algorithm</title><link>https://www.algorist.co.uk/tag/algorithm/</link></image><item><title>Latin Hypercube Sampling</title><link>https://www.algorist.co.uk/post/latin-hypercube-sampling/</link><pubDate>Fri, 18 Dec 2020 00:00:00 +0000</pubDate><guid>https://www.algorist.co.uk/post/latin-hypercube-sampling/</guid><description>
&lt;div id="what-is-lhs" class="section level2">
&lt;h2>What is LHS?&lt;/h2>
&lt;p>Latin hypercube sampling aims to bring the best of both worlds: the unbiased random sampling of monte carlo simulation; and the even coverage of a grid search over the decision space. It does this by ensuring values for all variables are as uncorrelated and widely varying as possible (over the range of permitted values).&lt;/p>
&lt;/div>
&lt;div id="why-do-i-need-one" class="section level2">
&lt;h2>Why do I need one?&lt;/h2>
&lt;p>The background to this is that I have been working on improving the convergence of an optimisation package that uses genetic algorithms. Having a good distribution of “genes”, or decisions within the initial population is key to allowing a GA to effectively explore the decision space. The &lt;code>ga()&lt;/code> function within the GA package in R allows for a &lt;em>suggestions&lt;/em> argument, which takes a matrix of decision values and places them within the starting population. Initially I wrote one function to create evenly spaced sequences for every decision between the lower and upper bound of allowable values, from which I enumerated all possible combinations using &lt;code>expand.grid()&lt;/code>. I then wrote another function to take a model object and a user-defined population size and automatically work out the nearest population count that allowed for even sampling over the model’s decision bounds.&lt;/p>
&lt;p>For models with large numbers of decisions the potential number of combinations is enormous. This is true even if you only select the upper and lower bound per decision. Already with 30 independent decisions with two possible values the number of combinations is 2^30 = 10,737,41,824, 10 times more than the number of stars in the average galaxy. Combinatorial algorithms are the worst type for growth in complexity (&lt;span class="math inline">\(O(n!)\)&lt;/span>). I needed a way of randomly sampling from the decisions but in a way that ensured the starting population had lots of diversity. This is the exact use case for LHS.&lt;/p>
&lt;/div>
&lt;div id="practical-examples" class="section level2">
&lt;h2>Practical examples&lt;/h2>
&lt;p>Let’s assume we have three decisions, where:&lt;/p>
&lt;ul>
&lt;li>decision a can be between 1 and 3,&lt;/li>
&lt;li>decision b can be TRUE or FALSE, and&lt;/li>
&lt;li>decision c can be red, green, blue or black&lt;/li>
&lt;/ul>
&lt;p>There are &lt;span class="math inline">\(3 * 2 * 4 = 24\)&lt;/span> possible unique combinations of these decisions (if a can only take on integer values). An individual in the starting population of a genetic algorithm would be of the form, &lt;code>1_TRUE_red&lt;/code>, for instance.&lt;/p>
&lt;p>Let’s assume we would like only 12 individuals from the 24 potential unique combinations, but we still want good representation of all/most possible decisions. Using the lhs library from R, we first create 12 random uniform distributions between 0 and 1 for each of the three decisions, a, b and c.&lt;/p>
&lt;pre class="r">&lt;code>library(lhs)
library(dplyr)
library(purrr)
# Test data
a &amp;lt;- 1:3
b &amp;lt;- c(TRUE, FALSE)
c &amp;lt;- c(&amp;quot;red&amp;quot;, &amp;quot;green&amp;quot;, &amp;quot;blue&amp;quot;, &amp;quot;black&amp;quot;)
all_decisions &amp;lt;- rbind(list(a,b,c))
sample &amp;lt;- as.data.frame(randomLHS(12, 3))
names(sample) &amp;lt;- c(&amp;quot;a&amp;quot;, &amp;quot;b&amp;quot;, &amp;quot;c&amp;quot;)
# Uniform random values between 0 and 1
sample&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## a b c
## 1 0.2298605 0.57586455 0.15879497
## 2 0.6123669 0.93655235 0.52807091
## 3 0.1405182 0.86291310 0.05353759
## 4 0.6784085 0.04476613 0.39006544
## 5 0.0777695 0.28693076 0.99092477
## 6 0.9939800 0.20343707 0.27813226
## 7 0.5170630 0.47209350 0.78766487
## 8 0.8783984 0.09234600 0.49824405
## 9 0.4196785 0.83133533 0.69800889
## 10 0.3825627 0.58463731 0.66272508
## 11 0.7590192 0.39640356 0.23725054
## 12 0.2929710 0.72958857 0.86157621&lt;/code>&lt;/pre>
&lt;p>Next we map the 0-1 distributions onto the real distributions of a, b and c. For instance, c has four possible values so the distribution should be 1-4. We also convert the values to factors in order to label them properly.&lt;/p>
&lt;pre class="r">&lt;code># Random choices for each decision with distributions based on a, b and c
choices &amp;lt;- map2(sample, all_decisions,
~cut(.x, length(.y), labels = .y)) %&amp;gt;%
bind_rows()
choices&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## # A tibble: 12 x 3
## a b c
## &amp;lt;fct&amp;gt; &amp;lt;fct&amp;gt; &amp;lt;fct&amp;gt;
## 1 1 FALSE red
## 2 2 FALSE blue
## 3 1 FALSE red
## 4 2 TRUE green
## 5 1 TRUE black
## 6 3 TRUE red
## 7 2 TRUE black
## 8 3 TRUE green
## 9 2 FALSE blue
## 10 1 FALSE blue
## 11 3 TRUE red
## 12 1 FALSE black&lt;/code>&lt;/pre>
&lt;p>For convenience we can bring this into a single function like so.&lt;/p>
&lt;pre class="r">&lt;code>lhs_sample &amp;lt;- function(decisions, n){
stopifnot(is.list(decisions))
len_decisions &amp;lt;- length(decisions)
samples &amp;lt;- lhs::randomLHS(n, len_decisions) %&amp;gt;%
as.data.frame()
names(samples) &amp;lt;- names(decisions)
choices &amp;lt;- purrr::map2(samples, decisions,
~cut(.x, length(.y), labels = .y))
bind_rows(choices)
}
lhs_sample(list(cars = rownames(mtcars), species = unique(iris$Species), letter = LETTERS[24:26]), n = 10)&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## # A tibble: 10 x 3
## cars species letter
## &amp;lt;fct&amp;gt; &amp;lt;fct&amp;gt; &amp;lt;fct&amp;gt;
## 1 Volvo 142E setosa Y
## 2 Mazda RX4 versicolor Z
## 3 Hornet Sportabout virginica X
## 4 Camaro Z28 setosa Z
## 5 Merc 450SLC setosa X
## 6 Chrysler Imperial versicolor Y
## 7 Toyota Corona virginica Z
## 8 Porsche 914-2 virginica X
## 9 Merc 240D setosa Z
## 10 Merc 450SE versicolor Y&lt;/code>&lt;/pre>
&lt;/div>
&lt;div id="continuous-distributions" class="section level2">
&lt;h2>Continuous distributions&lt;/h2>
&lt;p>The above example was for sampling from distributions where allowable values were either factors or whole integers. For continuous numerical distributions we can use the following.&lt;/p>
&lt;pre class="r">&lt;code>library(tidyr)
a_continuous &amp;lt;- all_decisions[[1]]
choices_continuous &amp;lt;- qunif(sample$a, min = min(a_continuous), max = max(a_continuous))
choices_continuous&lt;/code>&lt;/pre>
&lt;pre>&lt;code>## [1] 1.459721 2.224734 1.281036 2.356817 1.155539 2.987960 2.034126 2.756797
## [9] 1.839357 1.765125 2.518038 1.585942&lt;/code>&lt;/pre>
&lt;/div>
&lt;div id="testing-coverage" class="section level2">
&lt;h2>Testing coverage&lt;/h2>
&lt;p>In theory, each sample should be orthogonal, or independent, of each other sample to the greatest possible extent. Another way of putting it is that there should neither be any one value overrepresented or under-represented in the sample set. Let’s test this in practice:&lt;/p>
&lt;pre class="r">&lt;code>library(ggplot2)
big_sample &amp;lt;- lhs_sample(decisions = list(a = a,b = b,c = c), n = 1000)
ggplot(big_sample, aes(x = c, fill = c)) +
geom_bar() +
scale_fill_manual(values = c(&amp;quot;red&amp;quot;, &amp;quot;green&amp;quot;, &amp;quot;blue&amp;quot;, &amp;quot;black&amp;quot;)) +
labs(title = &amp;quot;For a single decision, each value has been sampled equally&amp;quot;)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.algorist.co.uk/post/latin-hypercube-sampling/index.en_files/figure-html/coverage_chart-1.png" width="672" />&lt;/p>
&lt;pre class="r">&lt;code>ggplot(big_sample, aes(x = a, y = b, colour = c)) +
geom_jitter() +
scale_colour_manual(values = c(&amp;quot;red&amp;quot;, &amp;quot;green&amp;quot;, &amp;quot;blue&amp;quot;, &amp;quot;black&amp;quot;)) +
labs(title = &amp;quot;Every value combination across all decisions has also been sampled equally&amp;quot;)&lt;/code>&lt;/pre>
&lt;p>&lt;img src="https://www.algorist.co.uk/post/latin-hypercube-sampling/index.en_files/figure-html/coverage_chart-2.png" width="672" />&lt;/p>
&lt;/div></description></item><item><title>Bias in algorithms</title><link>https://www.algorist.co.uk/post/bias-in-algorithms/</link><pubDate>Wed, 12 Aug 2020 00:00:00 +0000</pubDate><guid>https://www.algorist.co.uk/post/bias-in-algorithms/</guid><description>
&lt;p>Last week a colleague of mine shared the news that the UK Home Office has agreed to scrap its controversial ‘visa-streaming’ immigration algorithm after a successful legal challenge.&lt;/p>
&lt;blockquote class="twitter-tweet">&lt;p lang="en" dir="ltr">🚨Breaking news🚨&lt;br>&lt;br>We&amp;#39;ve got the Home Office to stop using its racist algorithm to sift visa applications!&lt;br>&lt;br>The algorithm gave “speedy boarding” to white people – the Home Office has been forced to scrap it after we &amp;amp; &lt;a href="https://twitter.com/Foxglovelegal?ref_src=twsrc%5Etfw">@foxglovelegal&lt;/a> launched legal action &lt;a href="https://t.co/qKSr6gEkGQ">https://t.co/qKSr6gEkGQ&lt;/a>&lt;/p>&amp;mdash; JCWI (@JCWI_UK) &lt;a href="https://twitter.com/JCWI_UK/status/1290561862807953412?ref_src=twsrc%5Etfw">August 4, 2020&lt;/a>&lt;/blockquote>
&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8">&lt;/script>
&lt;p>So what did it do? Essentially it classified applications according to a traffic light system. “Green light” applicants were fast-tracked through visa applications and “Red light” applicants were held to an increased level of scrutiny. Once assigned by the algorithm, the classification played a major role in the outcome of the visa application.&lt;/p>
&lt;p>So why the legal challenge? Surely a supervised classification algorithm with a low error rate when compared with historic decisions and human-assessed outcomes for the same application is a good thing? Not when an algorithm perpetuates institutional bias and sets up a toxic feedback loop of reinforced prejudice.&lt;/p>
&lt;p>In practice this meant that the traffic light system was highly correlated to whether an applicant was from a “suspect” country. Applications from these countries received more scrutiny, experienced more delay, and were more likely to be declined. The algorithm “learned” from decisions such as these, reinforcing the strength of this feature in future predictions.&lt;/p>
&lt;blockquote>
&lt;p>“The Home Office’s own independent review of the Windrush scandal, found that it was oblivious to the racist assumptions and systems it operates. This streaming tool took decades of institutionally racist practices, such as targeting particular nationalities for immigration raids, and turned them into software. The immigration system needs to be rebuilt from the ground up to monitor for such bias and to root it out.” Chai Patel, Legal Policy Director of JCWI&lt;/p>
&lt;/blockquote>
&lt;p>Whilst the upheld legal challenge is good news, it is a chilling reminder of the new challenges in the data era. Or is it an old challenge reframed? With all these new machine learning tools we simply have the ability to do what we always did but at scale, much more efficiently and much quicker. Sounds like all our private and public institutions could do with an algorithm audit.&lt;/p></description></item></channel></rss>