Commit 5e1767dc authored by Scott Lundberg's avatar Scott Lundberg
Browse files

First API code version.

parents
Loading
Loading
Loading
Loading

index.jl

0 → 100644
+142 −0
Original line number Diff line number Diff line
using ArgParse
s = ArgParseSettings()
@add_arg_table s begin
    "model"
        help = "Location of XGBoost model to use for prediction"
        arg_type = ASCIIString
        required = true
    "features"
        help = "Location of the feature names in the model"
        arg_type = ASCIIString
        required = true
    "train-features"
        help = "Location of training feature data for the model"
        arg_type = ASCIIString
        required = true
    "train-labels"
        help = "Location of training label data for the model"
        arg_type = ASCIIString
        required = true
    "source_dirs"
        help = "Location of the raw data csv files"
        arg_type = ASCIIString
        nargs = '+'
        required = true
    "--num-samples"
        help = "How many samples should be used to estimate the expectation for each held-out feature."
        arg_type = Int64
        default = 100
    "--ntree-limit"
        help = "Restrict the prediction to use at most this many trees."
        arg_type = Int64
        default = 10000
    "--quiet", "-q"
        help = "Don't print anything"
        action = :store_true
end
args = parse_args(s)

using Prescience
using JSON
using HttpServer
using XGBoost
using TimeZones
using StreamingTimeSeries

@assert all(map(isdir, args["source_dirs"])) "The given source_dirs were not all found!"

model = Booster(model_file = args["model"])
featureNames = ASCIIString[strip(l) for l in open(readlines, args["features"])]

# pre-define our date parsers
dformat_raw = Dates.DateFormat("yyyy-mm-ddTHH:MM:SS.sss")
utcTime = TimeZone("UTC")
dparse_utc(str::AbstractString) = ZonedDateTime(DateTime(str[1:end-1], dformat_raw), utcTime)

# pre-load the training data we will sample from
trainX = open(deserialize, args["train-features"])
trainLabels = open(deserialize, args["train-labels"])

# compute the base occurence rate so we can compare the predicted risks with it
baseRate = sum(trainLabels)/length(trainLabels)

# compute groups of features that consider as units
featureGroups = Dict()
for (i,feature) in enumerate(featureNames)
    matches = match(r"^(.*)_(ema|emv|decay|ts)[0-9\.]*$", feature)
    if matches != nothing
        featureGroups[matches[1]] = vcat(get(featureGroups, matches[1], Any[]), [i])
    else
        featureGroups[feature] = [i]
    end
end

# build a weighting matrix that forces the model to match the first sample
# this causes the sum of all features to be correct
d = ones(args["num-samples"])
d[1] = 1e8
W = spdiagm(d);

function explained_pred(procId, currTime)

    # load the events
    events = nothing
    for d in args["source_dirs"]
        fname = d*"/"*procId*".csv"
        if isfile(fname)
            events = open(dataevents, fname)
            break
        end
    end
    @assert events != nothing "$procId not found!"

    # find which events fall before the given time
    currPos = 1
    while currPos <= length(events) && events[currPos].time <= currTime
        currPos += 1
    end

    # compute the features
    features = Dict()
    updatefeatures!(features, events[1:currPos-1])
    data = Float32[haskey(features, k) ? valueat(features[k], currTime) : 0 for k in featureNames]
    data = reshape(data, 1, length(data))

    # find the model's output for this example
    origPred = XGBoost.predict(model, data, ntree_limit=args["ntree-limit"], output_margin=true)[1]

    # only check the importance of features that are present in this example (this is way faster)
    groupNames = collect(keys(featureGroups))
    filteredGroupNames = filter(x->!all(data[featureGroups[x]] .== 0), groupNames)

    # mask each feature group randomly for our samples
    randMasks = rand(args["num-samples"], length(filteredGroupNames)) .> 0.01;
    randMasks[1,:] = true # the first sample enforces correct output when all features are included

    # randomly replace entries in the data matrix that have been not masked
    dataMatrix = vcat([data for i in 1:args["num-samples"]]...)
    for i in 1:args["num-samples"]
        inds = vcat([featureGroups[filteredGroupNames[j]] for j in collect(1:length(filteredGroupNames))[vec(!randMasks[i,:])]]...)
        dataMatrix[i,inds] = trainX[rand(1:size(trainX)[1]),inds]
    end

    # compute the difference in model predictions from the base rate for each sample and solve for the additive effects
    y = XGBoost.predict(model, dataMatrix, ntree_limit=args["ntree-limit"], output_margin=true) - logit(baseRate)
    vals = inv(randMasks'*W*randMasks + 1e-8*I)*randMasks'*W*y

    Dict(
        "baseRate" => baseRate,
        "pred" => origPred,
        "featureEffects" => Dict(zip(filteredGroupNames, vals))
    )
end

http = HttpHandler() do req::Request, res::Response
    parts = split(req.resource, '/', limit=3)
    procId = parts[2]
    time = dparse_utc(parts[3])
    res.data = json(explained_pred(procId, time))
end

server = Server(http)
run(server, host=IPv4(127,0,0,1), port=5023)