# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .jl
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Julia 1.2.0
#     language: julia
#     name: julia-1.2
# ---

# <img src="https://julialang.org/assets/infra/logo.svg" alt="Julia" width="200" style="max-width:100%;">
#
# [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/alan-turing-institute/MLJ.jl/master?filepath=binder%2FMLJ_demo.ipynb)
#
# ## Lightning encounter with Julia programming language
#
# ###### Julia related content prepared by [@ablaom](https://github.com/ablaom)
#
# Interacting with Julia at the REPL, or in a notebook, feels very much
#
# the same as python, MATLAB or R:

print("Hello world!")

2 + 2

typeof(42.0)

# ## Just-in-time compilation
#
# Here's a function used in generating the famous Mandelbrot set,
#
# which looks pretty much the same in python, MATLAB or R:

function mandel(z)
    c = z
    maxiter = 80
    for n in 1:maxiter
        if abs(z) > 2
            return n-1
        end
        z = z^2 + c
    end
    return maxiter
end

# In particular, notice the absence of type annotations. The crucial difference is what happens when you call this function:

@time mandel(1.2)  # time call on a Float64

# This is actually pretty lousy, slower than python. However, trying again:

@time mandel(3.4)   # time on another Float64

# Thousands of times faster, second time around! What happenend?
#
# When you call `mandel(1.2)` in python, say, then the defining code
# is interpreted each time. When you call `mandel(1.2)` in Julia for
# the first time Julia inspects the of the argument, namely `Float64`,
# and using this information *compiles* an efficient type-specfic
# version of `mandel`, which it caches for use in any subsequent call
# *on the same type*. Indeed if we call `mandel` on a new type, a new
# compilation will be needed:

@time mandel(1.0 + 5.0im)

@time mandel(2.0 + 0.5im)

# Since plotting the Mandelbrot set means calling `mandel` millions of
# times on the same type, the advantage of just-in-time compilation is
# obvious.

# +
using PyPlot

plt.imshow([mandel(x + y * im) for y = -1:0.001:1, x = -2:0.001:1])
# -

# ## Multiple dispatch
#
# You will never see anything like `A.add(B)` in Julia because Julia
# is not a traditional object-oriented language. In Julia, function and
# structure are kept separate, with the help of abstract types and
# multiple dispatch, as we explain next
# In addition to regular concrete types, such as `Float64` and
# `String`, Julia has a built-in heirarchy of *abstract* types. These
# generally have subtypes but no instances:

typeof(42)

supertype(Int64)

supertype(Signed)

subtypes(Integer)

Bool <: Integer # is Bool a subtype of Integer?

Bool <: String

# In Julia, which is optionally typed, one uses type annotations to
# adapt the behaviour of functions to their types. If we define

divide(x, y) = x / y

# then `divide(x, y)` will make sense whenever `x / y` makes sense (for
# the built-in function `/`). For example, we can use it to divide two
# integers, or two matrices:

divide(1, 2)

divide([1 2; 3 4], [1 2; 3 7])

# To vary the behaviour for specific types we make type annotatations:

divide(x::Integer, y::Integer) = floor(x/y)
divide(x::String, y::String) = join([x, y], " / ")
divide(1, 2)

divide("Hello", "World!")

# In the case of `Float64` the original "fallback" method still
# applies:

divide(1.0, 2.0)

# ## User-defined types
#
# Users can define their own abstract types and composite types:

# +
abstract type Organism end

struct Animal <: Organism
    name::String
    is_hervibore::Bool
end

struct Plant <: Organism
    name::String
    is_flowering::Bool
end

describe(o::Organism) = string(o.name) # fall-back method
function describe(p::Plant)

    if p.is_flowering
        text = " is a flowering plant."
    else
        text = " is a non-flowering plant."
    end
    return  p.name*text

end
# -

describe(Animal("Elephant", true))

describe(Plant("Fern", false))

# ## Type inference and multiple dispatch
#
# *Type inference* is the process of identifying the types of the arguments to dispatch the right method.
#
# Blogpost about [type dispatch](http://www.stochasticlifestyle.com/type-dispatch-design-post-object-oriented-programming-julia/) by [<NAME>](http://www.chrisrackauckas.com/).

# +
function function_x(x::String)
    println("this is a string: $x")
end

function function_x(x::Int)
    println("$(x^2) is the square of $x")
end
# -

# each call to the function_x() will dispatch the corresponding method depending on the parameter's type
function_x("a string")
function_x(2)

# ## Automatic differentiation
#
# Differentiation of almost arbitrary programs with respect to their input. ([source]( https://render.githubusercontent.com/view/ipynb?commit=89317894e2e5370a80e45d52db8a4055a4fdecd6&enc_url=68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f6d6174626573616e636f6e2f454d455f4a756c69615f776f726b73686f702f383933313738393465326535333730613830653435643532646238613430353561346664656364362f315f496e74726f64756374696f6e2e6970796e62&nwo=matbesancon%2FEME_Julia_workshop&path=1_Introduction.ipynb&repository_id=270611906&repository_type=Repository#Automatic-differentiation) by [@matbesancon](https://github.com/matbesancon))

# +
using ForwardDiff

function sqrt_babylonian(s)
    x = s / 2
    while abs(x^2 - s) > 0.001
        x = (x + s/x) / 2
    end
    x
end
# -

sqrt_babylonian(2) - sqrt(2)

@show ForwardDiff.derivative(sqrt_babylonian, 2);
@show ForwardDiff.derivative(sqrt, 2);

# ## Unitful computations
# Physicists' dreams finally made true. ([soure](https://render.githubusercontent.com/view/ipynb?commit=89317894e2e5370a80e45d52db8a4055a4fdecd6&enc_url=68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f6d6174626573616e636f6e2f454d455f4a756c69615f776f726b73686f702f383933313738393465326535333730613830653435643532646238613430353561346664656364362f315f496e74726f64756374696f6e2e6970796e62&nwo=matbesancon%2FEME_Julia_workshop&path=1_Introduction.ipynb&repository_id=270611906&repository_type=Repository#Unitful-computations) by [@matbesancon](https://github.com/matbesancon))

using Unitful
using Unitful: J, kg, m, s

3J + 1kg * (1m / 1s)^2

# <img src="https://github.com/alan-turing-institute/MLJ.jl/raw/master/material/MLJLogo2.svg?sanitize=true" alt="MLJ" width="200" style="max-width:100%;">
#
# # MLJ
#
# MLJ (Machine Learning in Julia) is a toolbox written in Julia providing a common interface and meta-algorithms for selecting, tuning, evaluating, composing and comparing machine learning models written in Julia and other languages. MLJ is released under the MIT licensed and sponsored by the [Alan Turing Institute](https://www.turing.ac.uk/).

# ### The MLJ Universe
#
# The functionality of MLJ is distributed over a number of repositories
# illustrated in the dependency chart below.
#
# [MLJ](https://github.com/alan-turing-institute/MLJ) * [MLJBase](https://github.com/alan-turing-institute/MLJBase.jl) * [MLJModelInterface](https://github.com/alan-turing-institute/MLJModelInterface.jl) * [MLJModels](https://github.com/alan-turing-institute/MLJModels.jl) * [MLJTuning](https://github.com/alan-turing-institute/MLJTuning.jl) * [MLJLinearModels](https://github.com/alan-turing-institute/MLJLinearModels.jl) * [MLJFlux](https://github.com/alan-turing-institute/MLJFlux.jl) * [MLJTutorials](https://github.com/alan-turing-institute/MLJTutorials) * [MLJScientificTypes](https://github.com/alan-turing-institute/MLJScientificTypes.jl) * [ScientificTypes](https://github.com/alan-turing-institute/ScientificTypes.jl)
#
#
# <div align="center">
#     <img src="https://github.com/alan-turing-institute/MLJ.jl/raw/master/material/MLJ_stack.svg?sanitize=true" alt="Dependency Chart">
# </div>
#
# *Dependency chart for MLJ repositories. Repositories with dashed
# connections do not currently exist but are planned/proposed.*

# MLJ provides access to to a wide variety of machine learning models. For the most up-to-date list of available models `models()`.

using MLJ
models()

# ## Fit, predict, transform
#
# The following example is using the `fit()`, `predict()`, and `transform()` functions of MLJ.

import Statistics
using PrettyPrinting
using StableRNGs

X, y = @load_iris;

# let's also load the DecisionTreeClassifier:

@load DecisionTreeClassifier
tree_model = DecisionTreeClassifier()

# ## MLJ Machine
#
# In MLJ, a *model* is an object that only serves as a container for the hyperparameters of the model. A *machine* is an object wrapping both a model and data and can contain information on the *trained* model; it does *not* fit the model by itself. However, it does check that the model is compatible with the scientific type of the data and will warn you otherwise.

tree = machine(tree_model, X, y)

# A machine is used both for supervised and unsupervised model. In this tutorial we give an example for the supervised model first and then go on with the unsupervised case.
#
# ## Training and testing a supervised model
#
# Now that you've declared the model you'd like to consider and the data, we are left with the standard training and testing step for a supervised learning algorithm.
#
# ## Splitting the data
#
# To split the data into a training and testing set, you can use the function `partition` to obtain indices for data points that should be considered either as training or testing data:

rng = StableRNG(566)
train, test = partition(eachindex(y), 0.7, shuffle=true, rng=rng)
test[1:3]

# ## Fitting and testing the machine
#
# To fit the machine, you can use the function `fit!` specifying the rows to be used for the training:

fit!(tree, rows=train)

# Note that this **modifies** the machine which now contains the trained parameters of the decision tree. You can inspect the result of the fitting with the `fitted_params` method:

fitted_params(tree) |> pprint

# This `fitresult` will vary from model to model though classifiers will usually give out a tuple with the first element corresponding to the fitting and the second one keeping track of how classes are named (so that predictions can be appropriately named).
#
# You can now use the machine to make predictions with the `predict` function specifying rows to be used for the prediction:

ŷ = predict(tree, rows=test)
@show ŷ[1]

# Note that the output is probabilistic, effectively a vector with a score for each class. You could get the mode by using the `mode` function on `ŷ` or using `predict_mode`:

ȳ = predict_mode(tree, rows=test)
@show ȳ[1]
@show mode(ŷ[1])

# To measure the discrepancy between ŷ and y you could use the average cross entropy:

mce = cross_entropy(ŷ, y[test]) |> mean
round(mce, digits=4)

# # [Check out MLJ example with TreeParzen.jl](TreeParzen_example.ipynb)

# # A more advanced example

using MLJ
using StableRNGs
import DataFrames
@load RidgeRegressor pkg=MultivariateStats

# In this example we will show how to generate a model from a network; there are two approaches:
#
# * using the `@from_network` macro
# * writing the model in full
#
# the first approach should usually be the one considered as it's simpler.
#
# Generating a model from a network allows subsequent composition of that network with other tasks and tuning of that network.
#
# ### Using the @from_network macro
#
# Let's define a simple network
#
# *Input layer*

# +
rng = StableRNG(6616) # for reproducibility
x1 = rand(rng, 300)
x2 = rand(rng, 300)
x3 = rand(rng, 300)
y = exp.(x1 - x2 -2x3 + 0.1*rand(rng, 300))
X = DataFrames.DataFrame(x1=x1, x2=x2, x3=x3)
test, train = partition(eachindex(y), 0.8);

Xs = source(X)
ys = source(y, kind=:target)
# -

# *First layer*

# +
std_model = Standardizer()
stand = machine(std_model, Xs)
W = MLJ.transform(stand, Xs)

box_model = UnivariateBoxCoxTransformer()
box = machine(box_model, ys)
z = MLJ.transform(box, ys)
# -

# *Second layer*

ridge_model = RidgeRegressor(lambda=0.1)
ridge = machine(ridge_model, W, z)
ẑ = predict(ridge, W)

# *Output*

ŷ = inverse_transform(box, ẑ)

# No fitting has been done thus far, we have just defined a sequence of operations.
#
# To form a model out of that network is easy using the `@from_network` macro:

@from_network CompositeModel(std=std_model, box=box_model,
                             ridge=ridge_model) <= ŷ;

# The macro defines a constructor CompositeModel and attributes a name to the different models; the ordering / connection between the nodes is inferred from `ŷ` via the `<= ŷ`.
#
# **Note**: had the model been probabilistic (e.g. `RidgeClassifier`) you would have needed to add `is_probabilistic=true` at the end.

cm = machine(CompositeModel(), X, y)
res = evaluate!(cm, resampling=Holdout(fraction_train=0.8, rng=51),
                measure=rms)
round(res.measurement[1], sigdigits=3)

# ## Check out more [Data Science tutorials in Julia](https://alan-turing-institute.github.io/DataScienceTutorials.jl/).

StarcoderdataJupyterScriptsDedupFiltered