%% Treat Missing Data in a Credit Scorecard Workflow Using MATLAB® fillmissing
% This example shows a workflow to gather missing data, manually treat the training data, develop a new creditscorecard, and treat new data before scoring using the MATLAB® fillmissing.
% The advantage of this method is that you can use all the options available in fillmissing to fill missing data, as well as other MATLAB functionality such as standardizemissing and features for the treatment of outliers. In this approach, note that you must ensure that the treatment of the training data and the treatment of any new data set that requires scoring must be the same.
% Alternatively, after you create a creditscorecard object, you can use the fillmissing function for the creditscorecard object to fill missing values. For additional information on alternative approaches for "treating" missing data, see Credit Scorecard Modeling with Missing Values.
%
% The dataMissing table in the CreditCardData.mat file has two predictors with missing values — CustAge and ResStatus.

load CreditCardData.mat
head(dataMissing)

% First, analyze the missing data information using the untreated training data.
% Create a creditscorecard object using the CreditCardData.mat file to load the dataMissing that contains missing values. Set the 'BinMissingData' argument for creditscorecard to true to explicitly report information on missing values. Then apply automatic binning using autobinning.

sc = creditscorecard(dataMissing,'IDVar','CustID','BinMissingData',true);
sc = autobinning(sc);

%
% The bin information and bin plots for predictors that have missing data both show a <missing> bin at the end. The two predictors with missing values in this data set are CustAge and ResStatus.

bi = bininfo(sc,'CustAge');
disp(bi)
plotbins(sc,'CustAge')
bi = bininfo(sc,'ResStatus');
disp(bi)
plotbins(sc,'ResStatus')

%
% The missing bin can be left as is, although a common alternative is to treat the missing values. Note that treating the missing values must be done with care because it changes the data and can introduce bias.
% To treat missing values, you can apply different criteria. This example follows a straightforward approach to replace missing observations with the most common or typical value in the data distribution, which is the value of mode for the data. For this example, the mode happens to have a similar WOE value as the original <missing> bin. The similarity in values is favorable because similar WOE values means similar points in a scorecard.
% For CustAge, bin 4 is the bin with the most observations and the mode value of the original data is 43.

modeCustAge = mode(dataMissing.CustAge);
disp(modeCustAge)

% The WOE value of the <missing> bin is similar to the WOE value of bin 4. Therefore, replacing the missing values in CustAge with the value of mode is reasonable.
% To treat the data, create a copy of the data and fill the missing values.

dataTreated = dataMissing;
dataTreated.CustAge = fillmissing(dataTreated.CustAge,'constant',modeCustAge);

% For ResStatus, the value of 'Home Owner' is the value of the mode of the data, and the WOE value of the <missing> bin is closest to that of the 'Home Owner' bin.

modeResStatus = mode(dataMissing.ResStatus);
disp(modeResStatus)

% Use MATLAB® fillmissing to replace the missing data with 'Home Owner'.

dataTreated.ResStatus = fillmissing(dataTreated.ResStatus,'constant',string(modeResStatus));

% The treated data set now has no missing values.

disp(any(any(ismissing(dataTreated))))

%
% Using the treated data set, apply the typical creditscorecard workflow. First, create a creditscorecard object with the treated data and then apply automatic binning.

scTreated = creditscorecard(dataTreated,'IDVar','CustID');
scTreated = autobinning(scTreated);

% Compare the bin information of the untreated data for CustAge with the bin information of the treated data for CustAge.

bi = bininfo(sc,'CustAge');
disp(bi)
biTreated = bininfo(scTreated,'CustAge');
disp(biTreated)

% The first few bins are the same, but the treatment of missing values influences the binning results, starting with the bin where the missing data is placed. You can further explore your binning results using autobinning with a different algorithm or you can manually modify the bins using modifybins.
% For ResStatus, the results for the treated data look similar to the initial results, except for the higher counts in the 'Home Owner' bin due to the treatment. For a categorical variable with more categories (or levels), an automatic algorithm can find category groups and the results can show more differences for before and after the treatment.

bi = bininfo(sc,'ResStatus');
disp(bi)
biTreated = bininfo(scTreated,'ResStatus');
disp(biTreated)

%
% Fit the logistic model, scale the points, and display the final scorecard.

[scTreated, mdl] = fitmodel(scTreated,'Display','off');
scTreated = formatpoints(scTreated,'PointsOddsAndPDO',[500 2 50]);
ScPoints = displaypoints(scTreated);
disp(ScPoints)

%
% The new scorecard does not know that the data was treated, hence it assigns NaNs to the <missing> bins. If you need to score a new data set and it contains missing data, by default, the score function sets the points to NaN. To further explore the handling of missing data, take a few rows from the original data as test data and introduce some missing data.

tdata = dataTreated(11:14,mdl.PredictorNames); % Keep only the predictors retained in the model
% Set some missing values
tdata.CustAge(1) = NaN;
tdata.ResStatus(2) = '<undefined>';
tdata.EmpStatus(3) = '<undefined>';
tdata.CustIncome(4) = NaN;
disp(tdata)

% Score the new data and see how points are set to NaN, which leads to NaN scores.

[Scores,Points] = score(scTreated,tdata);
disp(Scores)
disp(Points)

%
% To assign points to missing data, one possibility is to use the name-value pair argument 'Missing' in formatpoints to choose how to assign points to missing values.
% Use the 'MinPoints' option for the 'Missing' argument. This option assigns the minimum number of possible points in the scorecard to the missing data. In this example, the minimum number of possible points for CustIncome is 29.906, so the last row in the table gets 29.906 points for the missing CustIncome value.

scTreated = formatpoints(scTreated,'Missing','MinPoints');
[Scores,Points] = score(scTreated,tdata);
disp(Scores)
disp(Points)

% However, for predictors treated in the training data, such as CustAge, the effect of the 'Missing' argument is inconsistent with the treatment of the training data. For example, for CustAge, the first observation gets 53.507 points for the missing value, yet if the new data were "treated," and the missing value for CustAge were replaced with the mode of the training data (age of 43), this observation falls in the [40,45) bin and receives 66.868 points.
% Therefore, before scoring, data sets must be treated the same way the training data was treated. The use of the 'Missing' argument is still important to assign points for untreated predictors and the treated predictors receive points in a way that is consistent with the way the model was developed.

tdataTreated = tdata;
tdataTreated.CustAge = fillmissing(tdataTreated.CustAge,'constant',modeCustAge);
tdataTreated.ResStatus = fillmissing(tdataTreated.ResStatus,'constant',string(modeResStatus));
disp(tdataTreated)
[Scores,Points] = score(scTreated,tdataTreated);
disp(Scores)
disp(Points)

% Copyright 2019 The MathWorks, Inc.