CViM / src / plots / utils.jl
utils.jl
Raw
# define constants for plots generation
const VALUE_GROUPS = ("C", "O", "SE", "ST")
SCENARIOS = ["Baseline", "C-skewed", "O-skewed", "SE-skewed", "ST-skewed"]
SHOCKS = ["Missing", "Corridor", "Corridor-unique", "Expansionary"]

# base theme settings
fontsize_theme = Theme(fontsize = 18, font = :bold_italic)
attributes = Attributes(
    Axis = (
        xgridvisible = false,
        ygridvisible = false,
        ytickalign = 1,
        xtickalign = 1,
    ),
)
set_theme!(merge(fontsize_theme, attributes))

## Auxiliary functions
function filtering(df::DataFrame, households::Bool, banks::Bool, value::Bool, status::Bool, relative::Bool, shock::String, vars_ib::Vector{Symbol})
    modified_df =
        if !relative
            if banks
                if value && !status
                    (@pipe df |> filter(:shock => ==(shock), _) |> dropmissing(_, vars_ib) |> filter(r -> r.status != "neutral", _)) 
                elseif !value || status 
                    (@pipe df |> filter(:shock => ==(shock), _) |> dropmissing(_, vars_ib))
                else
                    @warn "Not implemented"
                end
            else # not banks; variable :consumption is common to firms and households
                (@pipe df |> dropmissing(_, [:consumption]) |> filter([:id, :shock] => (x, y) -> ids_count(x, households) && y == shock, _))
            end
        else # relative
            if banks
                (@pipe df |> dropmissing(_, vars_ib) |> filter(:status => x -> x != "neutral", _))
            else # not banks; variable :consumption is common to firms and households
                (@pipe df |> dropmissing(_, [:consumption]) |> filter(:id => x -> ids_count(x, households), _))
            end
        end
    return modified_df
end

# helper function
ids_count(x::Int64, households::Bool) = households ? (x >= 1 && x <= number_of_households) :  (x > number_of_households && x <= number_of_households + number_of_firms)

function generate_df(df::DataFrame, m::DataFrame; 
    shock::String = "Missing",
    households::Bool = false,
    banks::Bool = false, 
    value::Bool = false, 
    status::Bool = false, 
    relative::Bool = false, 
    operation::Function = mean)

    # helper constants
    vars = (households && !banks) ? vars_hh : ((!households && banks) ? vars_ib : vars_firms)
    grouping_factors =
        if relative 
            [:step, :shock, :scenario]
        else # not relative
            if !value && !banks && !status
                [:step, :scenario] 
            elseif !value && banks && !status
                [:step, :status, :scenario]
            elseif value && !banks && !status
                [:step, :scenario, :value]
            elseif value && banks && !status
                [:step, :scenario, :value]
            elseif value && banks && status 
                [:step, :status, :scenario, :value]
            else
                @warn "Not implemented"
            end
        end

    filtered_df = @pipe df |> filtering(_, households, banks, value, status, relative, shock, vars) |>
                groupby(_, grouping_factors) |> 
                combine(_, vars .=> operation, renamecols = false)
    return filtered_df
end

function debt_to_gdp(df::DataFrame, df_firms::DataFrame)
    # Add debt_to_gdp column
    df[!, :debt_to_gdp] .= 0.0

    # Iterate over scenarios
    for scenario in unique(df.scenario)
        # Filter df_firms_sum to get the total GDP for the current scenario
        total_gdp = sum(filter(r -> r.scenario == scenario, df_firms).GDP)
        
        # Calculate debt-to-GDP ratio for the current scenario
        df[df.scenario .== scenario, :debt_to_gdp] .= (df[df.scenario .== scenario, :bills] ./ total_gdp) .* 100
    end
    return df
end

function standard_deviation_bands!(cycle, trend, colors)
    # Compute residuals from cyclicality of the time series
    residuals = cycle - trend
    # Compute the standard deviation of the residuals
    sigma = std(residuals)
    # Calculate upper and lower bands
    upper_bound = trend .+ sigma
    lower_bound = trend .- sigma

    # Plot the standard deviations as dashed lines
    lines!(lower_bound; color = (colors, 0.3), linestyle = :dash)
    lines!(upper_bound; color = (colors, 0.3), linestyle = :dash)
end

function add_lines!(gdf)
    # Add dotted lines to the plot for the specified variables in gdf
    lines!(gdf.icb; color = :grey, linewidth = 0.5)
    lines!(gdf.icbd; color = :grey, linewidth = 0.5)
    lines!(gdf.icbl; color = :grey, linewidth = 0.5)
end

function generate_dataframes(dataframes::Vector{DataFrame}, i::Int64, shock::String)
    gdf1 = @pipe dataframes[1] |> filter(:shock => ==("Missing"), _) |> groupby(_, :scenario)
    gdf1_shock = @pipe dataframes[1] |> filter(:shock => ==(shock), _) |> groupby(_, :scenario)
    gdf2 = @pipe dataframes[2] |> filter(:shock => ==("Missing"), _) |> groupby(_, :scenario)
    gdf2_shock = @pipe dataframes[2] |> filter(:shock => ==(shock), _) |> groupby(_, :scenario)
    gdf = i == 1 ? gdf1 : gdf2
    gdf_shock = i == 1 ? gdf1_shock : gdf2_shock
    return gdf, gdf_shock
end

function generate_dataframes(df::DataFrame, shock::String)
    gdf = @pipe df |> filter(:shock => ==("Missing"), _) |> groupby(_, :scenario)
    gdf_shock = @pipe df |> filter(:shock => ==(shock), _) |> groupby(_, :scenario)
    return gdf, gdf_shock
end

function invisible_yaxis!(fig, index)
    # Hide y-axis labels and ticks for subplots starting from the second subplot
    if index > 1
        fig.content[index].yticklabelsvisible = false
        fig.content[index].yticksvisible = false
    end
end

function configure_axes_links(fig, gdf)
    # Helper function to hide yaxis ticks and link content of multiple plots in a 5 plots fig
    if length(gdf) == length(SCENARIOS)
        start_idx = 2 
        end_idx = length(SCENARIOS)
        linkyaxes!(fig.content[start_idx:end_idx]...)
        invisible_yaxis!(fig, start_idx + 1)
        invisible_yaxis!(fig, end_idx)
    else
        linkyaxes!(fig.content...)
        for i in 1:length(fig.content)
            invisible_yaxis!(fig, i)
        end
    end
end

# Set features of axes; currently not used but useful to have plots having (i, j) rows for i in 1:length(vars) and j in 1:length(gdf)
function set_axes!(fig, gdf, vars, ylabels; status::Bool = false)
    index = 0 

    custom_length = status ? length(BY_STATUS) : length(vars)

    for i in 1:custom_length
        index += 1

        # Take the first plots of each row, i.e. (1,1), (2,1), (3,1), (4,1) etc...
        start_idx = (i - 1) * length(gdf) + 1
        # Take the last plots of each row 
        end_idx = start_idx + length(gdf) - 1 

        # Link axes for each variable grouped by shock
        linkyaxes!(fig.content[start_idx:end_idx]...)

        # Write ylabels for each variable only in the first plots of each row
        fig.content[start_idx].ylabel = ylabels[index]
       
        # Set up the alignment of ylabels
        fig.content[start_idx].alignmode = Mixed(left = 0)

        # Apply invisible_yaxis! only on specific plots
        for j in start_idx:end_idx
            mod_val = (j - start_idx + 1) % 4
            if mod_val == 2 || mod_val == 3 || mod_val == 0
                invisible_yaxis!(fig, j)
            end
        end

        # Set the x-label for the last group of subplots
        if i == length(vars)
            for k in (length(fig.content) - length(gdf) + 1):length(fig.content)
                fig.content[k].xlabel = "Steps"
            end
        end
    end

    # Set titles only in the first row of plots
    for i in 1:length(gdf)
        fig.content[i].title = only(unique(gdf[i].shock))
    end

end