ShapML.jl icon indicating copy to clipboard operation
ShapML.jl copied to clipboard

ShapML for Classification.

Open Rahulub3r opened this issue 2 years ago • 8 comments

Hi - is there a method to get shapley values for classification problems? The code I tried is below:

RFC = @load RandomForestClassifier pkg="DecisionTree" rfc_model = RFC() rf_machine = machine(rfc_model, X, y) MLJ.fit!(rf_machine) function predict_function(model, data) data_pred = DataFrame(y_pred = MLJ.predict(model, data)) return data_pred end explain = copy(X[1:50, :]) reference = copy(X) sample_size = 60 # Number of monte carlo samples data_shap = ShapML.shap(explain=explain, reference=reference, model=rf_machine, predict_function=predict_function, sample_size=sample_size, seed=1)

and I am getting the following error:

ERROR: LoadError: TypeError: in LocationScale, in T, expected T<:Real, got Type{Any} Stacktrace: [1] Distributions.LocationScale(μ::Float64, σ::Float64, ρ::UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}; check_args::Bool) @ Distributions ~/.julia/packages/Distributions/jEqbk/src/univariate/locationscale.jl:50 [2] Distributions.LocationScale(μ::Float64, σ::Float64, ρ::UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}) @ Distributions ~/.julia/packages/Distributions/jEqbk/src/univariate/locationscale.jl:47 [3] *(x::Float64, d::UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}) @ Distributions ~/.julia/packages/Distributions/jEqbk/src/univariate/locationscale.jl:126 [4] /(d::UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}, x::Int64) @ Distributions ~/.julia/packages/Distributions/jEqbk/src/univariate/locationscale.jl:129 [5] _mean(f::typeof(identity), A::Vector{UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}}, dims::Colon) @ Statistics /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.6/Statistics/src/Statistics.jl:176 [6] mean(A::Vector{UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}}; dims::Function) @ Statistics /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.6/Statistics/src/Statistics.jl:164 [7] mean(A::Vector{UnivariateFinite{OrderedFactor{2}, Int64, UInt32, Float64}}) @ Statistics /Users/julia/buildbot/worker/package_macos64/build/usr/share/julia/stdlib/v1.6/Statistics/src/Statistics.jl:164 [8] _predict(; reference::DataFrame, data_predict::DataFrame, model::Machine{MLJDecisionTreeInterface.RandomForestClassifier, true}, predict_function::typeof(predict_function), n_features::Int64, n_target_features::Int64, n_instances_explain::Int64, sample_size::Int64, precision::Nothing, chunk::Bool, reconcile_instance::Bool, explain::DataFrame) @ ShapML ~/.julia/packages/ShapML/QMund/src/predict.jl:30 [9] shap(; explain::DataFrame, reference::DataFrame, model::Machine{MLJDecisionTreeInterface.RandomForestClassifier, true}, predict_function::Function, target_features::Nothing, sample_size::Int64, parallel::Nothing, seed::Int64, precision::Nothing, chunk::Bool, reconcile_instance::Bool) @ ShapML ~/.julia/packages/ShapML/QMund/src/ShapML.jl:168 [10] top-level scope @ Untitled-1:21 in expression starting at Untitled-1:21

Rahulub3r avatar Nov 18 '21 16:11 Rahulub3r

Hi I managed to do this using an AMLPipeline: could you help me with the graph of feature importance? I am stuck with the Dataframes.by which should be now groupby!


catf = CatFeatureSelector() 
numf = NumFeatureSelector()
disc   = CatNumDiscriminator()
pt    = PrunedTree()
pca=SKPreprocessor("PCA");

pvote = disc |> ((catf |> ohe) + (numf|>pca)) |> pt

sample_size = 60  # Number of Monte Carlo samples.

AMLPipelineBase.fit!(pvote,X,y)
function predict_function(model, data)
    data_pred = DataFrame(y_pred = AMLPipelineBase.transform(model, data))
    return data_pred
  end

  data_shap = ShapML.shap(explain = X,
                        reference = X,
                        model = pvote,
                        predict_function = predict_function,
                        sample_size = sample_size,
                        seed = 1
                        )


show(data_shap, allcols = true)

michele2198 avatar Apr 04 '22 21:04 michele2198

I'm sorry about the bug and delayed response, but I'm afraid that I cannot support this repo right now--even though I'd really like to. In the meantime, I've given access to a collaborator and will likely transfer ownership to JuliaAI or the Alan Turing Institute soon.

To your specific questions, ShapML is not set up to work with classification. It wouldn't be all that difficult to add, but it's not something that I could take on right now.

nredell avatar Apr 12 '22 16:04 nredell

No worry, I made it work with classification, see above example, and also made the graph (I will post it in a few hours). Thanks best regards

michele2198 avatar Apr 12 '22 16:04 michele2198

The graph:


data_plot = DataFrames.groupby(data_shap, [:feature_name]);
        
        baseline = round(data_shap.intercept[1], digits = 1)
        using DataFramesMeta
        regr=sort(@combine(data_plot,:x2 = sum(:shap_effect)),order(:2,rev=true));
        
        using Gadfly  # Plotting
        p = Gadfly.plot(regr[1:10,:], y = :feature_name, x = :x2, Coord.cartesian(yflip = true),
                 Scale.y_discrete, Geom.bar(position = :dodge, orientation = :horizontal),
                 Theme(bar_spacing = 1mm),
                 Guide.xlabel("|Shapley effect| (baseline = $baseline)"), Guide.ylabel(nothing),
                 Guide.title("Feature Importance - Mean Absolute Shapley Value"))

michele2198 avatar Apr 12 '22 21:04 michele2198

@michele2198 would you mind posting full code for this? I am also trying to implement ShapML for multi class classification.

mattarnoldbio avatar Jul 20 '22 11:07 mattarnoldbio

Sure, here is all the code:

`using Pkg;
Pkg.add("XLSX")
using XLSX
import Base.Threads.@threads
#Pkg.add("MLDataPattern")
#Pkg.add("OutlierDetection")
#Pkg.add("OutlierDetectionData")
using MLDataPattern
#X_bal, y_bal = oversample((X, y));
#using OutlierDetection
#using OutlierDetectionData: ODDS
using Markdown
using InteractiveUtils
using Resample
import Pkg; 
#Pkg.add("PrettyPrinting");Pkg.add("PyPlot");Pkg.add("MLJ");Pkg.add("CSV");Pkg.add("EvoTrees");Pkg.add("UrlDownload");Pkg.add("MLJFlux");Pkg.add("DataFrames");Pkg.add("OutlierDetectionNetworks");Pkg.add("GLMakie");Pkg.add("MLJModels");Pkg.add("MLJScikitLearnInterface");Pkg.add("MLJMultivariateStatsInterface");Pkg.add("MLJNaiveBayesInterface");Pkg.add("NearestNeighborModels");Pkg.add("LightGBM");Pkg.add("MLJGLMInterface");Pkg.add("MLJLIBSVMInterface");
#Pkg.add("MLJBase");Pkg.add("CategoricalArrays");Pkg.add("Plots");Pkg.add("DecisionTree");Pkg.add("MLJDecisionTreeInterface");Pkg.add("BetaML");Pkg.add("MLJLinearModels");Pkg.add("MLJXGBoostInterface")
#Pkg.add("LIBSVM")
#Pkg.add("ScikitLearn")
#Pkg.add("ShapML")
#Pkg.add("DataFramesMeta")
#using UrlDownload
using MLJMultivariateStatsInterface
using DataFrames
using MLJModels
#using PrettyPrinting
using MLJBase
#using PyPlot
using MLJ
using CSV
using CategoricalArrays
using Plots
using FeatureSelectors
using MLBase
using MLJScikitLearnInterface
using ShapML
using AMLPipelineBase
using DataFramesMeta
using AutoMLPipeline
using Random

#KNN = @load DecisionTreeClassifier pkg=BetaML;
#KNN = @load XGBoostClassifier pkg=XGBoost;
KNN = @load RandomForestClassifier pkg=DecisionTree;
#KNN = @load AdaBoostStumpClassifier pkg=DecisionTree;
#KNN = @load LinearBinaryClassifier pkg=GLM; fa schifo
#KNN = @load EvoTreeClassifier pkg=EvoTrees;
#KNN = @load KNNClassifier pkg=NearestNeighborModels
#KNN = @load PegasosClassifier pkg=BetaML
#KNN = @load NeuralNetworkClassifier pkg=MLJFlux
#KNN = @load KNNClassifier pkg=NearestNeighborModels


function firstselection(Xred2,y)
    selector=MLJ.FeatureSelector();hot = MLJ.OneHotEncoder();stand=MLJ.Standardizer()
    MyPipe=stand|>selector|>hot|>KNN
    _names = schema(Xred2).names |> collect # make this a vector not tuple!
    #_ncols = length(_names)
    cases = [Symbol.(_names[1:i]) for i in 1:length(_names)]
    # wrap in a tuning strategy:    
    tuned_pipe = TunedModel(; model=MyPipe,
                            range=range(MyPipe, :(feature_selector.features), values=cases),
                            measures=[BrierScore()],#cross_entropy, Accuracy(), TruePositiveRate(), TrueNegativeRate()
                            resampling=CV(nfolds=5),
                            acceleration=CPUThreads(),
                            repeats=10)
                            
    mach = tuned_pipe_machine = machine(tuned_pipe, Xred2, y) |> MLJ.fit!
    return report(mach).best_model.feature_selector.features
end

function predict_function(model, data)
    data_pred = DataFrame(y_pred = AMLPipelineBase.transform(model, data))
    return data_pred
end


function featureselectShapley(Xred,y2)
    pvote =  ((catf |> ohe) + (numf)) |>rb|> pt
    #sample_size = 10  # Number of Monte Carlo samples.
    AMLPipelineBase.fit!(pvote,Xred,y2)
    
    data_shap = ShapML.shap(explain = Xred,
                    reference = Xred,
                    model = pvote,
                    predict_function = predict_function,
                    sample_size = 100,
                    seed = 1);

#data_plot = DataFrames.groupby(data_shap, [:feature_name])
#baseline = round(data_shap.intercept[1], digits = 1)
regr=sort(@combine(DataFrames.groupby(data_shap, [:feature_name]),:x2 = sum(:shap_effect)),order(:2,rev=true))
return regr.feature_name[1:numfeatures]
end

numf = NumFeatureSelector();disc   = CatNumDiscriminator();pt    = PrunedTree()
ohe = AMLPipelineBase.OneHotEncoder();catf = CatFeatureSelector() ;stack = StackEnsemble();ada   = Adaboost()
    rb=skoperator("RobustScaler");jrf=RandomForest();vote  = VoteEnsemble()

#https://juliaai.github.io/DataScienceTutorials.jl/end-to-end/breastcancer/

#url = "https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data";

data1=CSV.read("Tripleneg.csv", DataFrame)
df1 = DataFrame(data1)[:, 2:end];
y2, X = unpack(df1, ==(:outcome),name->true, rng = 0); #y2 vettore numerico puro

coerce!(df1, :outcome => OrderedFactor{2});    #fondamentale!!
y, X = unpack(df1, ==(:outcome),name->true, rng = 0);
X=convert.(Float32,X)
y1=y #vettore "ordered"

y=categorical(string.(y),ordered = true,levels = ["0", "1"])
X1=X[:,1:2]
describe(X)

numfolds=10
numfeatures=20
numMontecarlos=1


model_names=Vector{String}()
package_names=Vector{String}()
loss_acc=Float64[];
loss_sens=Float64[];
loss_spec=Float64[];


start = time()
function modelCVeval(clf,X,y,y1,y2)
        xx=length(y);
        rp = randperm(xx);
        Xshuffle=(X[rp,:]);yshuffle=y[rp];
        rows = collect(StratifiedKfold(yshuffle, numfolds))       
        #collect(ScikitLearn.CrossValidation.StratifiedKFold(y, n_folds=5, shuffle=true, random_state=1))            
        local yCV=[];
        local predCV=[];                                 
        for i=1:numfolds
            #println("i = $i on thread $(Threads.threadid())")
            local sss=Vector(1:size(yshuffle,1))
            local train=rows[i]
            local test=vec(sss[setdiff(1:size(sss,1), rows[i]), :])

            #----------------UnivariateFeatureSelector-------------------
            X_bal, y_bal = undersample((Matrix(Xshuffle[train,:])', yshuffle[train]));y1_bal=categorical(int.(Array(y_bal)).-1);y2_bal=(int.(Array(y_bal)).-1);
            X_test=Xshuffle[test,:];X_bal=DataFrame(Matrix(X_bal'),names(Xshuffle));
            #local Xred=X[:,select_features(UnivariateFeatureSelector(method=f_test, k=10),X[train,:],Array(y1[train]))];
            local Xred=X_bal[:,select_features(UnivariateFeatureSelector(method=pearson_correlation, k=numfeatures),X_bal,Array(y1_bal))];
            
            #SHAPLEY-----------------------------
            #select!(Xred,featureselectShapley(Xred[train,:],y2[train]))
            select!(Xred,featureselectShapley(Xred,y2_bal))
            
            #°°°°°°°°°°°°°°°feature selection of top features------------------------
            select!(Xred,firstselection(Xred,y_bal))
            
            #select!(Xred,featureselectShapley(Xred[train,:],y2[train]))
          

            #training the classifier!!!!!!!!!!!!!!!!!!!!
            clf_machine = machine(Standardizer()|> clf(), Xred, y_bal)
            MLJBase.fit!(clf_machine);

            append!(predCV,MLJ.predict(clf_machine, X_test[:,names(Xred)]));
            append!(yCV,yshuffle[test]);
            end #end numfolds
    #Obtaining different evaluation metrics

return predCV,yCV
end

for m in [models(matching(X1, y1))[1:5];(name="NeuralNetworkClassifier",package_name="MLJFlux")]#Threads.@threads #NeuralNetworkClassifier pkg=MLJFlux
    model_name=m.name
    package_name=m.package_name
    eval(:(clf = @load $model_name pkg=$package_name verbosity=0))
    for nn=1:numMontecarlos
    if m.name≠"DSADDetector"
        if m.name≠"ESADDetector"#è unsupervised
            if m.name≠"GaussianProcessClassifier"#non funzia!
                if m.name≠"PerceptronClassifier" #non funziona!!
    (predCV,yCV)=modelCVeval(clf,X,y,y1,y2)
  
    if m.name≠"RidgeCVClassifier"&&m.name≠"RidgeClassifier"&&m.name≠"SVC"&&m.name≠"SVMClassifier"&&m.name≠"SVMLinearClassifier"&&m.name≠"SVMNuClassifier"&&m.name≠"SGDClassifier"&&m.name≠"PassiveAggressiveClassifier"&&m.name≠"NuSVC"&&m.name≠"LinearSVC"&&m.name≠"DeterministicConstantClassifier"
        
      
        acc = MLJ.accuracy(mode.(predCV), yCV)
        #f1_score = MLJ.f1score(mode.(predCV), yCV)
        sens = MLJBase.true_positive_rate(parse.(Int,string.(mode.(predCV))), parse.(Int,string.(yCV)))
        f1_score =MLJ.f1score(parse.(Int,string.(mode.(predCV))), parse.(Int,string.(yCV)))
        spec = MLJBase.true_negative_rate(parse.(Int,string.(mode.(predCV))), parse.(Int,string.(yCV)))
        end
        
        if m.name=="RidgeCVClassifier"||m.name=="RidgeClassifier"||m.name=="SVC"||m.name=="SVMClassifier"||m.name=="SVMLinearClassifier"||m.name=="SVMNuClassifier"||m.name=="SGDClassifier"||m.name=="PassiveAggressiveClassifier"||m.name=="NuSVC"||m.name=="LinearSVC"||m.name=="DeterministicConstantClassifier"
          
            acc = MLJ.accuracy((predCV), yCV)
            #f1_score = MLJ.f1score(mode.(predCV), yCV)
            sens = MLJBase.true_positive_rate(parse.(Int,string.((predCV))), parse.(Int,string.(yCV)))
            f1_score =MLJ.f1score(parse.(Int,string.((predCV))), parse.(Int,string.(yCV)))
            spec = MLJBase.true_negative_rate(parse.(Int,string.((predCV))), parse.(Int,string.(yCV)))
            end
        push!(model_names, m.name)
    push!(package_names,m.package_name)
    append!(loss_acc, acc)
    append!(loss_sens, sens)
    append!(loss_spec, spec)
      
end;end;end;end;
    
end
end
elapsed = time() - start

learners=vcat(DataFrame(mname=model_names,pname=package_names,sensCV=loss_sens,specCV=loss_spec,accCV=loss_acc))
@show learners
@show elapsed

@combine(DataFrames.groupby(learners,[:mname,:pname]),:aveacc=mean(:accCV))

#CSV.write("File Name.csv", learners)

XLSX.writetable("df.xlsx", collect(DataFrames.eachcol(learners)), DataFrames.names(learners))
`

michele2198 avatar Jul 20 '22 21:07 michele2198

Thanks for this. Ours is a multi class classification problem, so I will have to have a little play around and see if I can adapt it to that. Can you think of any reason why that shouldn't work?

mattarnoldbio avatar Jul 21 '22 11:07 mattarnoldbio

no, consider that shapley is not designed for binary class, but for regression. So I don't see reason to not work with multiclass

my code is designed to predict the "outcome" column of the csv file which is 0 or 1

michele2198 avatar Jul 21 '22 13:07 michele2198