Tutorials
Published on 14 Dec 2022
Data scientists don't usually start writing full-blown web apps from the get-go. Instead, we begin with the typical including data cleaning, exploratory analysis, testing of several models, etc., and only when we're confident in the quality of the insights extracted from the data do we start thinking about publicly sharing them with a web application. The Genie Framework comes in very handy for this last step as it allows you to implement an interactive web UI so that:
All of this without writing a single line of HTML or Javascript thanks to Genie Builder's no-code editor.
In this week's tutorial, I'll show you how to go from your existing Julia code to a Genie app in just a few steps. The code for the example app can be downloaded here, take a look at the animation below to see what it looks like in a browser.
To illustrate the Genie workflow, we'll write some code to extract insights from the German Credit Risk dataset. This dataset contains information about 1000 people, where each person is described by 20 features such as age, sex or credit amount. Additionally, every person has been manually labelled as "good credit risk" or "bad credit risk" according to the feature values.
Say we want to extract the following insights from the dataset:
To calculate these metrics, save the code below to a script germancredits.jl
and execute it in a Julia REPL to make sure everything is working.
using DataFrames, Dates, OrderedCollections
function good_bad_credits_stats(data::DataFrame)
good_credits_count = data[data.Good_Rating .== true, [:Good_Rating]] |> nrow
bad_credits_count = data[data.Good_Rating .== false, [:Good_Rating]] |> nrow
good_credits_amount = data[data.Good_Rating .== true, [:Amount]] |> Array |> sum
bad_credits_amount = data[data.Good_Rating .== false, [:Amount]] |> Array |> sum
(; good_credits_count, bad_credits_count, good_credits_amount, bad_credits_amount)
end
function credit_age_amount(data::DataFrame; good_rating::Bool)
data[data.Good_Rating .== good_rating, [:Age, :Amount]]
end
function credit_no_by_age(data::DataFrame; good_rating::Bool)
age_stats::LittleDict{Int,Int} = LittleDict()
for x in 20:10:90
age_stats[x] = data[(data.Age .∈ [x:x+10]) .& (data.Good_Rating .== good_rating), [:Good_Rating]] |> nrow
end
age_stats
end
# testing the functions
using CSV
data = CSV.File("german_credits.csv") |> DataFrame
@show good_bad_credits_stats(data)
@show credit_age_amount(data, good_rating=true)
@show credit_no_by_age(data, good_rating=true)
Now that the analysis code is finished and working, it's time to make it into a Genie app.
We'll use the Genie Builder plugin for VSCode to build the app. After installing the plugin, start the GB server and create a new app named GermanCredits. Click yes when asked if you want to start it, and the no-code editor will appear.
In the workspace tab, you'll find that the newly-created app's code is split into two files: app.jl
and app.jl.html
. Of the two, app.jl
is the one to be manually edited whereas app.jl.html
is written to by the no-code editor. At this point, you can add the script germancredits.jl
to the workspace to have all your files in one place.
To avoid conflicts, it is recommended to wrap the code in a module and remove any testing code. So, edit germancredits.jl
to make it look like this:
module GermanCredits
export good_bad_credits_stats, credit_age_amount_duration, credit_data_by_age
using DataFrames, Dates, OrderedCollections
function good_bad_credits_stats(data::DataFrame)
...
end
function credit_age_amount(data::DataFrame; good_rating::Bool)
...
end
function credit_no_by_age(data::DataFrame; good_rating::Bool)
...
end
end
To install the required packages DataFrames
, Dates
and OrderedCollections
within the app, launch Genie Builder's Package Manager by right-clicking on the app's name on the left sidebar. You'll see the installation progress in the REPL, and the package versions will be added to the Project.toml
file.
Next, we'll design the UI to display the data insights.
For the German Credits UI, we want to have:
To build the UI with the no-code editor, look for the components you need in the right sidebar and drag them onto the canvas. Start with the header, rows and columns to set the page layout:
Next, add the following components:
Finally, let's make the font of the number badges bigger. Click on the </>
icon and change the class of the <p>
element to bignumber
. Then, add a new CSS style .bignumber{font-size:40px; text-align:center;}
.
To preview the UI, click on the monitor icon next to the GET tab in the left sidebar. Note that nothing works yet, you still have to implement the app's logic and bind the UI components to the Julia code.
To make the app interactive you have to implement its logic by:
The file app.jl
in the workspace is the main entry point to the app, and it's where you'll write the logic. This is what the file looks like by default:
using GenieFramework
@genietools
@handlers begin
@out message = "Hello World!"
@onchange isready begin
@show "App is loaded"
end
end
@page("/", "app.jl.html")
To start with a clean app, delete the contents of the block delimited by the @handlers
macro. Then, include the GermanCredits package and load the dataset by adding the following code before it:
include("germancredits.jl")
using .GermanCredits
using GenieFramework, CSV, DataFrames, OrderedCollections
const data = CSV.File("german_credits.csv") |> DataFrame
Next, we'll add the reactive variables and code for each element in the UI. Since we want to filter the data to show only a certain age range, let's start with the range selector.
In Genie apps, reactivity is based around two concepts:
@in
macro if they take their value from the UI, or with the @out
macro if they output their value to the UI.@onchange
macro, which watches a variable and whenever it changes executes the code in the block.To filter the data via the range selector in the UI, we need:
RangeData
to store the range selected in the UI.DataFrame
to hold the filtered data.To implement these elements, add the following code inside the @handlers
macro:
@handlers begin
@in age_range::RangeData{Int} = RangeData(18:90)
@out filtered_data = DataFrame()
@onchange age_range begin
filtered_data = data[(age_range.range.start .<= data[!, :Age] .<= age_range.range.stop), :]
end
end
Note that the variables have default values so that the UI can be rendered when the page loads. Now, go to the no code editor, select the Range component and assign the age_range
variable to its Data Binding field.
That's it for the range selector. Any time you move one end of the slider in the UI, the contents of filtered_data
will be updated. Let's continue with the next element to show the updated insights.
To display the number and amount in credits, you'll need:
good_bad_credits_stats
to update the variables when a new range is selected. @handlers begin
...
@out good_credits_count = 0
@out bad_credits_count = 0
@out good_credits_amount = 0
@out bad_credits_amount = 0
@onchange age_range begin
...
good_credits_count, bad_credits_count, good_credits_amount,
bad_credits_amount = good_bad_credits_stats(filtered_data)
end
end
To show the number badges in the UI, assign each variable to the Text Binding field of a Paragraph component.
We'll plot the credits by age with a bar plot indexed by the age ranges in 10 year increments. For this, you'll need:
PlotData
to hold the plot data.PlotLayout
to specify the plot layout.credit_no_by_age
. @handlers begin
...
@out age_slots = ["20-30", "30-40", "40-50", "50-60", "60-70", "70-80", "80-90"]
@out credit_no_by_age_plot = PlotData[]
@out credit_no_by_age_plot_layout = PlotLayout(barmode="group", showlegend = true)
@onchange age_range begin
...
credit_no_by_age_plot = [
PlotData(
x = age_slots,
y = collect(values(credit_no_by_age(filtered_data; good_rating = true))),
name = "Good credits",
plot = StipplePlotly.Charts.PLOT_TYPE_BAR,
marker = PlotDataMarker(color = "#72C8A9")
),
PlotData(
x = age_slots,
y = collect(values(credit_no_by_age(filtered_data; good_rating = false))),
name = "Bad credits",
plot = StipplePlotly.Charts.PLOT_TYPE_BAR,
marker = PlotDataMarker(color = "#BD5631")
)
]
end
end
For this plot, you'll require:
PlotData
to hold the plot data.PlotLayout
to specify the plot layout. @handlers begin
...
@out age_amount_plot_layout = PlotLayout(showlegend = true)
@out age_amount_plot = PlotData[]
@onchangeany age_range begin
...
dgood = credit_age_amount(filtered_data; good_rating = true)
dbad = credit_age_amount(filtered_data; good_rating = false)
age_amount_plot = [
PlotData(
x = dgood.Age,
y = dgood.Amount,
name = "Good credits",
mode = "markers",
marker = PlotDataMarker(size=18, opacity= 0.4, color = "#72C8A9", symbol="circle")
),
PlotData(
x = dbad.Age,
y = dbad.Amount,
name = "Bad credits",
mode = "markers",
marker = PlotDataMarker(size=18, opacity= 0.4, color = "#BD5631", symbol="cross")
)
]
end
end
And with this last component, you've finished your Genie app!
You can run your app locally within VSCode from the Genie Builder Apps menu in the sidebar. However, if you want to run your app outside of Genie Builder, the app's files are located in the folder ~/.julia/geniebuilder/apps/GermanCredits/
. Before running the app, you'll have to tell Genie to start a server by adding the following line at the end of app.jl
:
Server.isrunning() || Server.up()
Then, open a REPL with julia --project
in the app's folder and type include("app.jl")
to launch the app.