R Puzzle No. 2 – Where’s the zebra?
[This article was first published on U.N.A. Matemáticas El Tigre, and kindly contributed to Rbloggers]. (You can report issue about the content on this page here)
Want to share your content on Rbloggers? click here if you have a blog, or here if you don't.
Want to share your content on Rbloggers? click here if you have a blog, or here if you don't.
For the second R puzzle, we will tackle the famous Zebra Puzzle, sometimes known as Einstein’s Puzzle or Einstein’s Riddle. There are various implementations of solutions to the puzzle in different programming languages^{1}, but here we will be using R and an R package called lpSolve, which is intended to solve linear programming problems. I had a lot of fun preparing this entry and learned quite a bit on how to implement linear programming models using R – I hope you will find this post entertaining, interesting and useful.
The Puzzle
The Zebra Eye, by Singapore artist Jarrell Goh aka fuzzyzebra (2007).

The Zebra puzzle, or Einstein’s riddle as it is often called, was popularized in a 1962 number of Life International magazine, with a followup in a March 1963 issue of Life containing the solution. It is said to have been invented by a young Einstein, although it is also credited to Lewis Carroll. The truth is, there is no clear evidence of either’s authorship of the riddle. I should also point out that various versions of the puzzle exist, with different nationalities, brands of cigarettes, or even replacing sports teams with cigarettes, etc.
The version of the puzzle we will be using here is as follows. There are five houses along a street whose five occupants are all of different nationalities (English, German, Norwegian, Swedish, and Danish). Each occupant also smokes a different brand of cigarette (five brands in all), has a different favorite drink, has a different pet and owns a house of a different color than his neighbors. The following clues are given:


The Englishman lives in the red house.

The Swede has a dog.

The Dane drinks tea.

The man in the green house drinks coffee.

The white house on the left of the green house.

The man in the yellow house smokes Dunhill.

The man who smokes Pall Mall has a bird.

The German smokes Prince.

The man who drinks beer smokes Bluemaster.

The man that smokes Blends lives next to the man with a cat.

The man with a horse lives next to the one that smokes Dunhill.

The man who smokes Blends lives next to the man that drinks water.

The Norwegian lives next to blue house.

The man in the middle house (third house) drinks milk.

The Norwegian lives in first house.
If you have never tried to solve this puzzle, you should. It’ll give you some moments of good entertainment. I suggest you make a table with 5 columns and 5 rows in which the columns represent the five houses, numbered 15 from left to right and the rows represent the nationalities, cigarette brands, pets, favorite drinks and house colors. Start with the most direct clues and incrementally reach the solution by using one clue at a time. Once you reach the solution, reflect on how you did it. What algorithm did you use? How did you represent the data? And finally, how would you program a computer (using R specifically) to solve this puzzle? This is when the real fun begins…
A mathematical approach
Like I said, there are various code implementations in various programming languages that you can find on the Internet. On the rosetta.org site (listed in the References section), there’s even an R solution using an Rpackage called “combinat”, which I confess I’m not familiar with but I assume it’s similar to the Python “constraints” module. In this post I’ll take a different approach (using linear programming), but first, let’s lay down the basic strategy for solving this puzzle computationally.
If you tried to solve the puzzle by hand using a 5×5 table like the one I suggested, you may have probably considered the blank table cells as the problem variables, to be filled by a specific drink, nationality or other characteristic. If we are to solve this puzzle by coding it, we should take another approach: we should consider instead the 25 characteristics (brands of cigarettes, nationalities, house colors and so on) as the variables, each variable assuming an integer value between 1 and 5 representing the number of the house where that specific item is located. Doing so brings our puzzle within the realm of mathematics.
Without any restrictions, we simply assign to each of the 25 variables a number between 1 and 5. There are \(5^{25}\approx 2.98\cdot 10^{17}\) possible ways of doing so. If we next assume that there are no two houses with occupants of the same nationality, pets, brands of cigarettes, etc., then we narrow the solution space down to \((5!)^5=24,883,200,000\) possible cases, which is still a considerable number of cases. Each of the puzzle’s clues is really an additional constraint and together, these constraints reduce the possibilities to a single definitive solution.
So once we have defined our 25 variables identifying them according to the specific item they represent, such as Horse, Norwegian, Tea, Red, and so on, how do we mathematically represent the constraints? Consider the first clue for example: the Englishman lives in the red house. This can be represented mathematically as \(English=Red\). Clues like “the occupant of the middle house (third house) drinks milk” can be expressed as \(Milk=3\). If the white house is to the right of the green house, we could simply write this as \(White=Green + 1\). If the Norwegian lives in the first house, we represent this by \(Norwegian=1\). The “x is next to y”type clues are a bit trickier though.
What does “x is next to y” or “x and why are neighbors” mean? It simply means that their house numbers differ by one. So, for example, “the Norwegian lives next to the blue house” could be expressed mathematically as \(NorwegianBlue=1\), which in turn means that either \(NorwegianBlue=1\) or \(BlueNorwegian=1\). Finally, since we require all variables to be bounded between 1 and 5, we could impose these constraints as inequalities: \(Red\geq 1\) and \(Red\leq 5\), for all 25 variables. All in all, what I mean to show you is that all constraints can be expressed as a system of equations and inequalities. More specifically, these equations and inequalities are all linear.
This is where linear programming comes in to the picture. In linear programming, we attempt to maximize or minimize an objective (linear) function, usually representing a cost as a function of the decision variables, when we impose constraints as a system of linear inequalities like \(Ax\leq b\) or \(Ax\geq b\). In linear programming, \(A_{n\times m}\) is a matrix of coefficients whose m columns represent the m decision variables and the n rows represent the n constraints as inequalities. On the righthand side, b is a vector whose n components represent the righthand side of each constraint inequality.
To give an idea about what a linear programming problem looks like, I’ll use the following example from Taha (1982): A paint factory that produces interior and exterior paints uses two types of raw materials (A and B). The maximum availability of A is 6 tons per day, whereas for B it is 8 tons per day. To produce 1 ton of exterior paint, they require 1 ton of raw material A and 2 tons of raw material B, whereas for 1 ton of interior paint, they require 2 tons of A and 1 ton of B. Market surveys indicate that the daily demand for interior paint cannot exceed that of exterior paint by more than 1 ton. The survey also indicates that the maximum demand for interior paint is limited to 2 tons daily. Furthermore, the wholesale price per ton of interior paint is
$
2000 and for exterior paint it is $
3000. How much interior and exterior paint should the factory produce to maximize gross income?
In the example problem given, it is clear that our two decision variables are \(X_I\) and \(X_E\) representing the daily production (in tons) of interior and exterior paint respectively. Then we could express the usage restrictions on the usage of raw materials and maximum raw material availability as follows:
\[\begin{align*} X_E + 2X_I &\leq 6 \quad \text{(raw material A)}\\ 2X_E + X_I &\leq 8 \quad \text{(raw material B)} \end{align*}\]
The demand restrictions (according to the market surveys), could be expressed as
\[\begin{align*} X_E + X_I &\leq 1 \quad & \text{(the demand for interior paint cannot exceed}\\ {} & {}\qquad & \text{that of exterior paint by more than 1)}\\ X_I &\leq 2 \quad & \text{(the daily demand for interior paint is less than 2)} \end{align*}\]
And then of course there are the standard nonnegativity restrictions on the decision variables that are tacitly assumed in any linear program: \(X_E, X_I \geq 0\). We want to maximize the gross income, and this income is given by \(z=3000X_E+2000X_I\), which is a linear function on the decision variables. All in all, our linear program would be:
Maximize \(z(X)=Z\cdot X\), subject to \(AX\leq B\), where \(X=(X_E,X_I)\), \(Z=(3000,2000)^T\), \(B=(6,8,1,2)^T\), and \(A\) is the following matrix of coefficients:
\[A=\left(\begin{array}{cc} 1 & 2\\ 2 & 1\\ 1 & 1\\ 0 & 1\\ \end{array}\right)\]
With \(X_E, X_I\geq 0\). Alternatively, in nonmatrix form, our problem would be to maximize \(z=3000X_E+2000X_i\) subject to the following restrictions:
\[\begin{align*} 1X_E + 2X_I &\leq 6\\ 2X_E + 1X_I &\leq 8\\ 1X_E + 1X_I &\leq 1\\ 0X_E + 1X_I &\leq 2 \end{align*}\]
Again, subject to the nonnegativity restrictions on \(X_E\) and \(X_I\).
The lpSolve package
It is not my intention in this post to give a thorough explanation of how the simplex algorithm used in linear programming works, nor delve into the principle of duality in linear programs. My intention is a more pragmatic and modest one to show how to use one of the R packages dedicated to linear programming to solve such problems. Of course, knowing the inner workings of the mathematical tools could be useful in some cases, but in most reallife scenarios, it suffices to be able to identify the tool needed, to set up the problem in terms of the inputs that the tool requires, and to be able to assess and interpret the output. Specifically, we will make use of the “lpSolve” R package, which is an R wrapper to the lp_solve version 5.5 functions freely available under the LGPL 2 software license^{2}. So, to continue with the example problem we defined above, the R code for solving it would be as follows:
1 2 3 4 5 6 7 8 
library(lpSolve) z < c(3000,2000) B < c(6,8,1,2) A < matrix(c(1,2,2,1,1,1,0,1),ncol=2,byrow=TRUE) colnames(A) < c("X_E","X_I") dir < rep("<=",4) (result < lp("max", z, A, dir, B)) result$solution 
Success: the objective function is 12666.67
[1] 3.333333 1.333333
In the code above, we obtained a maximum gross income of $12666.67 with 3.33 tons of exterior paint (\(X_E\)) and 1.33 tons of interior paint (\(X_I\)). In line 4 we define the constraints matrix
A
. In the next line, we give names to the columns that correspond to the decision variables. This is not necessary, but the user must be aware that the solution vector will be given in terms of these variables in the order that they appear on the constraints matrix. Notice that the lp
function call in line 7 requires we specify whether we want a maximum or minimum for the objective function, in this case given by the vector z
defined in line 2. A neat feature of the lp
function is that for each constraint row of the constraints matrix, we can specify a direction for the inequality as "
, "="
or ">="
, so not all restriction inequalities have to be of the same type.
Granted, our zebra puzzle is quite more complicated than this example problem. It involves at least 25 decision variables (the 5 nationalities, 5 pets, 5 colors, 5 cigarette brands and 5 drinks). But having seen how we mathematically expressed some of the restriction clues, it is clear that they all involve inequalities or equalities, which suits us fine because the
lp
function in lpSolve can handle any of those three types of restrictions for each constraint separately.
An important point to consider though, is that we are only interested in integer solutions for the decision variables. Although classic linear programming is intended to solve for real solutions on the decision variables, there are variants of the linear programming method, called integer linear programming, which restrict the solutions to integer values. Fortunately,
lp
can handle these cases and even the more restricted cases where the decision variable is binary (either 0 or 1). As we shall see, it is very easy to indicate which variables we want to be real, integer, or binary. So on that aspect of our zebra problem requiring that our variables be restricted to integer values (1,2,3,4 or 5) indicating the house where each item is, we have nothing to worry about.
Not so fast, cowboy
It would seem like just by invoking the lpSolve library, our zebra problem has been solved. But it isn't. For one thing, how can we efficiently and most simply represent the 25 decision variables and build the constraints matrix? Do we have to build this matrix manually, perhaps using a spreadsheet program and reading it in as a csv file from R? Instead of doing that, I decided to use a neat feature of R which allows me to index elements of a vector by using characterstring names instead of integer numbers. Such vectors are called named vectors. So what I will do is first define a character vector with all the variable names. For each row in the restriction matrix, I will start with a numeric vector of 25 elements all set to 0 and name that vector with the variable names I defined previously. If I want to set a particular element of that constraint row to a certain value, I just simply index it by the variable name instead of using a number. To keep things simple as I show you how to do this, we will pretend that we only have four decision variables: "W","X","Y" and "Z".
To begin with, how would we define the constraint "X is in house 3"? As we have previously seen, that is tantamount to \(X=3\), so our constraint matrix row would be defined in R as follows:
variable_names < c("W","X","Y","Z") row1 < rep(0,4) names(row1) < variable_names row1["X"] < 1 row1
W X Y Z <br /> 0 1 0 0
The named vector
row1
contains the coefficients for the constraint. The direction of that constraint would be "="
, and the corresponding element of the righthand side vector of the (in this case) equality would be 3. Now say we want to indicate that "W and Z are in the same house". That would be equivalent to stating that \(W=Z\) or \(WZ=0\). In that case we would add a second row to are constraint matrix as follows:
row2 < rep(0,4) names(row2) < variable_names row2["W"] < 1 row2["Z"] < 1 row2
W X Y Z <br /> 1 0 0 1
As in the previous case of
row1
, the direction (sign) of that constraint would again be "="
and the corresponding righthand side value would be 0, since we want \(WX=0\). You may be thinking that perhaps this is a complicated way to do this, but consider that with 25 variables per linear constraint where most of the coefficients are zero, it makes a lot of sense to set the coefficients only of one or two variables we are dealing with. In effect, we are seeing that the constraints matrix will be a sparse matrix (mostly filled with zeros). Constraints like "W is to the right of Y" are mathematically expressed as \(WY=1\) and it wouldn't be too hard to see how that constraint row would be defined in R.
Now when we come to a constraint like "X and Y are next to each other", that means that they differ by 1: \(XY\leq 1\). This absolutevalue inequality results in two simultaneous inequalities: \(XY \leq 1\) and \(YX\leq 1\). These two inequalities can be easily represented as constraint rows in R as we have seen. The catch is that \(1\leq XY \leq 1\) does not exclude the possibility that \(XY=0\), that is, that X and Y be equal. However if "X and Y are next to each other, they cannot occupy the same house, so to the \(XY\leq 1\) constraint we need to add \(XY\neq 0\). The problem is, how do we add a strict inequality constraint to a linear program when our only "direction" operators are
"
, "="
and ">="
?
We want X and Y to be different (not equal), and since X and Y are limited to integer values, this is equivalent to stating that \(XY\geq 1\), which in turn means that either \(XY\geq 1\) or \(YX\leq 1\). However, we cannot have an "either/or" disjunction condition in a linear program, since all the conditions must be met simultaneously (through the "and" conjunction). We could use de Morgan's laws and state that transform these statements as I show below, , but we would just be going around in circles.
\[\begin{align*} XY\geq 1\;&\vee\;XY\leq 1 \equiv\\ \neg (XY\lt 1\;&\wedge\;XY\gt 1) \equiv\\ XY&\neq 0 \end{align*}\]
Fortunately, there's a "trick" that I discovered on the lp_solve reference guide webpage^{3} that one can use to incorporate constraints like \(XY\geq 1\) into a linear program. To generalize as much as possible in what follows, we will assume that \(X=a_1X_1 + \ldots + a_mX_m\) is a linear expression on the decision variables. We want to be able to express \(X\geq min\) as a valid linear programming constraint. The trick is to introduce an additional binary decision variable \(B\) that can only assume two values: 0 or 1.
\[\begin{align*} X+M\cdot B &\geq min\qquad (1) \\ X+M(1B) &\geq min \end{align*}\]
In the above, \(M\) is simply a big, positive constant. How big does it have to be is something we will discover shortly. For now, bear with me and let's see what happens if \(B=0\). In that case, the inequalities in (1) become:
\[\begin{align*} X&\geq min \qquad (1a)\\ X+M &\geq min \end{align*}\]
In this case (\(B=0\)), \(X\geq min\) and since we are assuming that M is a large enough constant, the second condition will always be fulfilled. If \(B=1\), then (1) is changed to the following:
\[\begin{align*} X+M&\geq min \qquad (1b)\\ X&\geq min \end{align*}\]
In that case (\(B=1\)), the condition \(X \geq min\) must hold, which can only mean that \(X\) is positive (X was negative). The first condition in (1b) automatically holds because we have been assuming that M is large enough anyways. So how large is "large enough"? Well, from either (1a) or (1b), we can see that \(M\geq min+X\). While we could in theory use a really large value for M like \(1\cdot 10^{20}\), it is not advisable to do so because that would create numerical instabilities with our linear program solver.
If we can somehow put an upper bound to \(X\) in our problem, then we can find a feasible value for M. In our zebra puzzle, say we have two variables like "Norwegian" and "Blue" and we want to express that these have different values \(NorwegianBlue\geq 1\). By looking at the expression within the absolute value brackets \(NorwegianBlue\), we would have to see how large the difference can become. Since all decision variables in our problem are within the 15 range, we can see that this difference, in absolute value, cannot be larger than 4. So that means that any value of M larger than \(4+1=5\) will suffice. In my R solution, I'll be using a value of 20 for M. At any rate, a "x not equal to y constraint" implies using one additional binary decision variable and two additional constraint rows, as shown in (1), in the linear program. Now we are ready to tackle the zebra puzzle in R, so without further ado, here's the script.
And finally, the R solution
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
#define the variables, each assuming integer values between 1 and 5 nationalities < c("Dane","English","German","Norwegian","Sweede") drinks < c("Beer","Coffee","Milk","Tea","Water") smokes < c("Blend","Bluemaster","Dunhill","Pallmall","Prince") pets < c("Bird","Cat","Dog","Horse","Zebra") house_color < c("Blue","Green","Red","White","Yellow") variable_names < c(nationalities,drinks,smokes,pets,house_color) #add binary variables for defining inequalities: #4 for the problem constraints #50 for requiring that the variables within each group #(colors, nationalities, pets, etc.) have to be different variable_names < c(variable_names,sprintf("B%02d",1:54)) #define maximum bound for inequalities M < 20 #define Binary variable counter b_counter < 1 #define blank constraints, dir and rhs constraints < NULL dir < character(0) rhs < numeric(0) #functions for defining constraints x_is_y < function(x,y) { #x and y are in the same house new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 new_row[y] < 1 constraints << rbind(constraints,new_row) row.names(constraints) << NULL dir << c(dir,"=") rhs << c(rhs,0) } x_is_in_n < function(x,n) { #x is in house number n new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 constraints << rbind(constraints,new_row) row.names(constraints) << NULL dir << c(dir,"=") rhs << c(rhs,n) } x_to_left_of_y < function(x,y) { #x is on the house to the right of y new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 new_row[y] < 1 constraints << rbind(constraints,new_row) row.names(constraints) << NULL dir << c(dir,"=") rhs << c(rhs,1) } x_is_not_y < function(x,y) { #function that defines x and y as not equal #1st inequality constraint new_row < numeric(length(variable_names)) names(new_row) < variable_names B_variable < sprintf("B%02d",b_counter) new_row[x] < 1 new_row[y] < 1 new_row[B_variable] < M constraints << rbind(constraints,new_row) dir << c(dir,">=") rhs << c(rhs,1) #2nd inequality constraint new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 new_row[y] < 1 new_row[B_variable] < M constraints << rbind(constraints,new_row) dir << c(dir,">=") rhs << c(rhs,1M) row.names(constraints) << NULL b_counter << b_counter+1 } x_is_next_to_y < function(x,y) { #x is next to y (x and y are neighbors) new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 new_row[y] < 1 constraints << rbind(constraints,new_row) dir << c(dir,"<=") rhs << c(rhs,1) #2nd neighboring constraint row new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 new_row[y] < 1 constraints << rbind(constraints,new_row) dir << c(dir,"<=") rhs << c(rhs,1) #now define inequality constraints x_is_not_y(x,y) } inequality_within_group < function(var_group) { #function that defines all variables within a group #to be different from each other. #first obtain all possible pair of group variable combinations group_combinations < t(combn(var_group,2)) #now loop through this table, adding constraints for (i in 1:nrow(group_combinations)) { x_is_not_y (group_combinations[i,1],group_combinations[i,2]) } } variable_bounds < function(x) { #function that creates constraints for bounds of #problem variable (must be in 15 range) new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 constraints << rbind(constraints,new_row) dir << c(dir,">=") rhs << c(rhs,1) new_row < numeric(length(variable_names)) names(new_row) < variable_names new_row[x] < 1 constraints << rbind(constraints,new_row) dir << c(dir,"<=") rhs << c(rhs,5) row.names(constraints) << NULL } #Now define constraints in Prolog fashion x_is_y("English","Red") x_is_y("Sweede","Dog") x_is_y("Dane","Tea") x_is_y("Coffee","Green") x_is_y("Dunhill","Yellow") x_is_y("Pallmall","Bird") x_is_y("German","Prince") x_is_y("Bluemaster","Beer") x_to_left_of_y("White","Green") x_is_next_to_y("Blend","Cat") x_is_next_to_y("Dunhill","Horse") x_is_next_to_y("Blend","Water") x_is_next_to_y("Norwegian","Blue") x_is_in_n("Milk",3) x_is_in_n("Norwegian",1) inequality_within_group(nationalities) inequality_within_group(drinks) inequality_within_group(smokes) inequality_within_group(pets) inequality_within_group(house_color) for (v in variable_names[1:25]) variable_bounds(v) #Finally invoke the lp solve to solve the linear program #and output results library(lpSolve) obj < rep(0,79) result < lp("max", obj, constraints, dir, rhs, int.vec=1:25,binary.vec=26:79) result < result$solution[1:25] result_table < NULL for (i in 1:5) { result_table < cbind(result_table, variable_names[which(result==i)]) } colnames(result_table) < paste("House",1:5) row.names(result_table) < c("Nationality","Drinks","Smokes","Pet","House color") result_table 
House 1 House 2 House 3 House 4 House 5 <br /> Nationality "Norwegian" "Dane" "English" "Sweede" "German"<br /> Drinks "Water" "Tea" "Milk" "Beer" "Coffee"<br /> Smokes "Dunhill" "Blend" "Pallmall" "Bluemaster" "Prince"<br /> Pet "Cat" "Horse" "Bird" "Dog" "Zebra" <br /> House color "Yellow" "Blue" "Red" "White" "Green"
So the zebra is in house number 5 (green colored), where the Coffee drinking, Prince smoking German guy lives.
Notes

There is a thorough list of coding implementations (with source code in various languages) of the Zebra Puzzle at the rosettacode.org site (see WILLNESS et al).

See BERKELAAR (2015).

See BERKELAAR (2010).
Bibliography

BERKELAAR, M. et al. (2010). lp_solve 5.5 reference guide: Absolute values. http://lpsolve.sourceforge.net/5.5/asolute.htm.

BERKELAAR, M. et al. (2015). Interface to 'Lp_solve' v. 5.5 to Solve Linear/Integer Programs. R package "lpSolve". http://cran.rproject.org/.

GOH, J. (2007) The Zebra eye [Digital visualization]. Retrieved from http://fav.me/d11cbfy.

R DEVELOPMENT CORE TEAM (2009). R: A Language and Environment for Statistical Computing. R Foundation for Statistical Computing, Vienna, Austria. ISBN 3900051070. http://www.Rproject.org.

TAHA, H. (1982). Operations Research  An Introduction. 3rd Edition. New York: MacMillan Publishing Co.

WILLNESS et al. (2011). Zebra Puzzle. https://rosettacode.org/wiki/Zebra_puzzle.
.

Zebra Puzzle. (2016, August 18). In Wikipedia, The Free Encyclopedia. Retrieved 17:01, October 1, 2016, from https://en.wikipedia.org/w/index.php?title=Zebra_Puzzle&oldid=735058353
If you found this post interesting or useful, please share it on Google+, Facebook or Twitter so that others may find it too.
To leave a comment for the author, please follow the link and comment on their blog: U.N.A. Matemáticas El Tigre.
Rbloggers.com offers daily email updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/datascience job.
Want to share your content on Rbloggers? click here if you have a blog, or here if you don't.