Logistic Regression
Logistic regression predicts a binary (1/0) outcome — here, whether a debit-card transaction is fraudulent — from one or
more predictors. The dataset is FraudRisk.csv, 1,827 transactions split close to evenly between fraud and legitimate.
The copy used for this module ships with the categorical
Type field already pivoted into Is_Chip, Is_Swipe and Is_Manual 0/1 columns rather than as a raw character
column; Procedure 1 reconstructs a Type character column from those flags first, purely so the pivoting technique
itself can be demonstrated against real data. Two packages beyond the shared set were installed into the validation
container’s library path: ROCR (Procedures 8 and 9) and gmodels
(Procedure 6). dplyr and ggplot2 are both in the shared set already.
Procedure 1: Pivot a Categorical Variable for Regression Analysis
Character and numeric-label fields need pivoting before they are useful to a model that expects numbers.
factor() is the labour-saving route: it converts a character column into a set of levels R itself knows how to use in
a formula, without pivoting every distinct value into its own column by hand.
FraudRisk <- read.csv("/work/FraudRisk.csv", stringsAsFactors = FALSE)
Type <- ifelse(FraudRisk$Is_Chip == 1, "Chip", ifelse(FraudRisk$Is_Swipe == 1, "Swipe", "Manual"))
str(Type)
FraudRisk <- mutate(FraudRisk, Type)
TypeFactor <- factor(FraudRisk$Type)
table(TypeFactor)
FraudRisk <- mutate(FraudRisk, TypeFactor)
Console output:
chr [1:1827] "Chip" "Chip" "Chip" "Chip" "Chip" "Chip" "Chip" "Chip" ...
TypeFactor
Chip Manual Swipe
1087 217 523
A single condition can also be pivoted by hand with ifelse(), without going through factor() at all — useful when
only one category matters, or as part of horizontal abstraction:
IsHighRisk <- ifelse(FraudRisk$Type == "Manual", 1, 0)
table(IsHighRisk)
FraudRisk <- mutate(FraudRisk, IsHighRisk)
Console output:
IsHighRisk
0 1
1610 217
Tool equivalent: Jube.Service.Agent.AdvancedAnalyticsTools.DataPreparationAndResampling.EncodingTools
/ tool "FitOneHotEncoder" (with "ApplyOneHotEncoder" for new rows) — expands a categorical column into one 0/1
column per distinct category, named <column>=<category>, in one call over the whole table rather than a factor()
plus a manual ifelse() per level. IsHighRisk above is exactly the
Type=Manual column of that output with dropFirst left false; there is no separate tool for pivoting a single
condition by hand. A single categorical column reduced to one integer code (rather than one 0/1 column per level) is
"FitLabelEncoder", covered in DataStructures.md Procedure 14.
Procedure 2: Create an Abstraction Deviation Independent Vector
Comparing a value against the typical value for that vector is one of the highest-value transformations in behavioural analytics. A Z score is the usual choice, but a range (min-max) normalisation suits skewed data better: it places a value on a 0–1 scale relative to the smallest and largest values observed.
Min_Count_Transactions_1_Day <- min(FraudRisk$Count_Transactions_1_Day)
Max_Count_Transactions_1_Day <- max(FraudRisk$Count_Transactions_1_Day)
c(Min_Count_Transactions_1_Day, Max_Count_Transactions_1_Day)
Range_Deviation_Count_Transactions_1_Day <- (FraudRisk$Count_Transactions_1_Day -
Min_Count_Transactions_1_Day) / (Max_Count_Transactions_1_Day -
Min_Count_Transactions_1_Day)
summary(Range_Deviation_Count_Transactions_1_Day)
FraudRisk <- mutate(FraudRisk, Range_Deviation_Count_Transactions_1_Day)
Console output:
[1] 1 26
Min. 1st Qu. Median Mean 3rd Qu. Max.
0.0000 0.0400 0.1200 0.1785 0.2800 1.0000
Tool equivalent: Jube.Service.Agent.AdvancedAnalyticsTools.DataPreparationAndResampling.ScalingTools
/ tool "FitTableScaler" with method: MinMax (paired with "ApplyTableScaler" for new rows) — for every column in
one call, (value - minimum) / (maximum - minimum) using the fitted Centers (the minimums) and Scales (the ranges),
so training and live rows are normalised identically instead of the minimum and maximum being hand-carried in two loose
vectors. Standard (z-score), RobustMad and
RobustIqr are alternative methods on the same tool for the less-skewed case the module mentions in passing.
Procedure 3: Fit a one-way Log Curve on a Plot
Before modelling, it helps to see whether a candidate predictor actually discriminates the outcome. Plotting a 0/1
dependent variable against a candidate predictor puts every point on y = 0 or y = 1
with nothing in between; overlaying a glm(family = "binomial") smooth turns that into a curve showing the estimated
probability of fraud across the predictor’s range.
library(ggplot2)
qplot(FraudRisk$Count_Unsafe_Terminals_1_Day, FraudRisk$Dependent)
ggsave("/work/plot1.png", width = 5, height = 4)
qplot(FraudRisk$Count_Unsafe_Terminals_1_Day, FraudRisk$Dependent) +
stat_smooth(method = "glm", method.args = list(family = "binomial"))
ggsave("/work/plot2.png", width = 5, height = 4)
Console output:
Warning message:
`qplot()` was deprecated in ggplot2 3.4.0.
`geom_smooth()` using formula = 'y ~ x'
`geom_smooth()` using formula = 'y ~ x'
AdvancedAnalytics equivalent: Jube.Charting, Chart.Roc() — the question this procedure is really asking (“does
this one predictor separate fraud from legitimate?”) is exactly what a ROC curve answers, more precisely than eyeballing
a fitted sigmoid over a scatter of 0/1 points. Chart.Roc().FromScores("Count_Unsafe_Terminals_1_Day",
actual, rawValues).Render() treats the raw candidate predictor itself as the score, computes the curve (handling ties,
thinning to at most 2,000 points) and puts the AUC in the legend with the best cut-off (Youden J) marked, so the
discrimination judgement is a number and a curve instead of a shape read off a smoothed line. Never ggplot2 or a raw R
graphics device for this in the product — see Charting.md.
Procedure 4: Forward Stepwise Logistic Regression
glm() with family = "binomial" fits a logistic regression the same way lm() fits a linear one, plus the family
argument. summary() gives the model output; coefficients() gives the numbers with enough precision to deploy by
hand. Adding the factor from Procedure 1 extends it to a multiple logistic regression, one dummy coefficient per
non-reference level.
LogisticRegressionModel <- glm(Dependent ~
Count_Unsafe_Terminals_1_Day, data = FraudRisk, family = "binomial")
summary(LogisticRegressionModel)
coefficients(LogisticRegressionModel)
LogisticRegressionModelStepwise <- glm(Dependent ~
Count_Unsafe_Terminals_1_Day + TypeFactor, data = FraudRisk, family = "binomial")
coefficients(LogisticRegressionModelStepwise)
Console output:
Call:
glm(formula = Dependent ~ Count_Unsafe_Terminals_1_Day, family = "binomial",
data = FraudRisk)
Coefficients:
Estimate Std. Error z value Pr(>|z|)
(Intercept) -0.82552 0.06253 -13.20 <2e-16 ***
Count_Unsafe_Terminals_1_Day 0.44032 0.02692 16.36 <2e-16 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Dispersion parameter for binomial family taken to be 1)
Null deviance: 2532.4 on 1826 degrees of freedom
Residual deviance: 1976.7 on 1825 degrees of freedom
AIC: 1980.7
Number of Fisher Scoring iterations: 5
(Intercept) Count_Unsafe_Terminals_1_Day
-0.8255244 0.4403157
(Intercept) Count_Unsafe_Terminals_1_Day
-1.1538985 0.2956514
TypeFactorManual TypeFactorSwipe
1.0340266 1.8307673
Tool equivalent: two paths, and neither does automated forward/backward variable selection for a
binomial model — "StepwiseLinearRegression"
(Jube.Service.Agent.AdvancedAnalyticsTools.RelationshipsAndRegression.RegressionModelSelectionTools) is OLS-only, and
EliminateFeaturesRecursively
(Jube.Service.Agent.AdvancedAnalyticsTools.DataPreparationAndResampling.FeatureSelectionTools) ranks by a linear-model
coefficient, not a logistic one. What the product does have:
GeneralisedLinearModelTools.FitGeneralisedLinearModel(familyBinomial, linkLogit, its default) fits exactly this model — deviance, null deviance, pseudo R², dispersion, log-likelihood, AIC, BIC, a likelihood-ratio test against intercept-only, and a coefficient table with odds ratios — in one call over whichever predictors are supplied; there is no growing formula, because the whole predictor set is given up front. (FitProbitRegressionandFitComplementaryLogLogRegressionon the same class are binomial siblings with a different link, not a logit-specific shortcut.)LogisticRegressionTools.AssessPredictorSignificanceis the tool-layer’s stand-in for “assume the next strongest correlating factor”: it screens every candidate predictor on its own against the 0/1 outcome and returns each one’s odds ratio, Wald p-value, Benjamini-Hochberg adjusted p-value and raw-variable ROC AUC, so the choice of what to add next is read off a ranked table instead of guessed at.- For the classifier-grade version of this same fit (train/validation/test split, calibration, held-out metrics, a
stored snapshot) see Procedure 5’s mapping,
TrainLogisticRegression.
Procedure 5: Recalling a Logistic Regression Model
predict.glm() scores every row of a data frame against a fitted model, the same signature as
predict.lm().
AutomaticLogisticRegression <- predict.glm(LogisticRegressionModel, FraudRisk)
head(AutomaticLogisticRegression)
FraudRisk <- mutate(FraudRisk, AutomaticLogisticRegression)
Console output:
1 2 3 4 5 6
0.05510711 -0.82552437 -0.82552437 -0.82552437 -0.82552437 0.05510711
Tool equivalent: this single glm()-and-predict.glm() pairing is where the product stops being a
one-call statistics tool and becomes a governed, versioned model, and it comes in two forms:
Jube.Service.Agent.AdvancedAnalyticsTools.LogisticRegressionTools/ tool"TrainLogisticRegression"fits with a train/validation/test split, optional ridge penalty and class weighting, Platt calibration on the validation rows, and returns a JSON model snapshot together with held-out metrics, a coefficient table with odds ratios, and diagnostic warnings — the interactive path, capped at 2,000 rows, with nothing persisted: the caller holds the snapshot and passes it back into"RecallLogisticRegression"to score new rows, exactly fillingpredict.glm()’s role but against a model that was itself evaluated on data it never trained on.Jube.Service.Agent.AdvancedAnalyticsTools.LogisticRegressionTrainingRunTools/ tool"SubmitLogisticRegressionTraining"queues the same fit as a background run againstJube.Service.Repository.AnalyticsTrainingRun.AdvancedAnalyticsTrainingService(reached throughAdvancedAnalyticsTrainingRunsGateway, which implementsIAdvancedAnalyticsTrainingRunssolely for logistic regression — it is not shared with the thirty-one trainable model families inIntroduction.md, each of which has its own dedicated training service). This path does persist a trained model and version row (TrainedModelId,TrainedModelVersionId), reachable via"GetTrainingRunResult", though training never makes that version live — promotion is a separate, deliberate step. Use this path once a dataset is bigger than the 2,000-row synchronous cap, or already sits in the Advanced Analytics cache.
Logistic regression is therefore neither purely a stateless GLM tool call nor a training service identical in shape to
the thirty-one model families: it is its own dedicated persisted-model pathway, plus a separate nothing-persisted
quick-fit path for exploration, both distinct from
FitGeneralisedLinearModel’s plain one-shot regression fit in Procedure 4.
Procedure 6: Activating Logistic Regression and Creating a Confusion Matrix
A logistic regression’s raw output is a log-odds score, roughly -5 to +5, where 0 is the 50/50 point. Activating at 0
turns the score into a 1/0 prediction; table() (or, with friendlier output, gmodels’s
CrossTable()) compares that prediction against the actual outcome.
ActivateAutomaticLogisticRegression <- ifelse(AutomaticLogisticRegression > 0, 1, 0)
FraudRisk <- mutate(FraudRisk, ActivateAutomaticLogisticRegression)
table(FraudRisk$Dependent, FraudRisk$ActivateAutomaticLogisticRegression)
library("gmodels")
CrossTable(FraudRisk$Dependent, FraudRisk$ActivateAutomaticLogisticRegression)
Console output:
0 1
0 818 108
1 311 590
Cell Contents
|-------------------------|
| N |
| Chi-square contribution |
| N / Row Total |
| N / Col Total |
| N / Table Total |
|-------------------------|
Total Observations in Table: 1827
| FraudRisk$ActivateAutomaticLogisticRegression
FraudRisk$Dependent | 0 | 1 | Row Total |
--------------------|-----------|-----------|-----------|
0 | 818 | 108 | 926 |
| 105.563 | 170.746 | |
| 0.883 | 0.117 | 0.507 |
| 0.725 | 0.155 | |
| 0.448 | 0.059 | |
--------------------|-----------|-----------|-----------|
1 | 311 | 590 | 901 |
| 108.492 | 175.483 | |
| 0.345 | 0.655 | 0.493 |
| 0.275 | 0.845 | |
| 0.170 | 0.323 | |
--------------------|-----------|-----------|-----------|
Column Total | 1129 | 698 | 1827 |
| 0.618 | 0.382 | |
--------------------|-----------|-----------|-----------|
Tool equivalent: the activation-and-count-table work is already done for you.
"TrainLogisticRegression"’s TestMetrics (LogisticSplitMetrics,
Jube.Engine.AdvancedAnalytics.Regression) carries TruePositives, FalsePositives, TrueNegatives,
FalseNegatives, Precision, Recall, F1 and MatthewsCorrelation at a chosen or cost-minimising
Threshold, computed once alongside every other held-out metric rather than requiring a separate
table() or CrossTable() call against a manually activated vector. "CrossValidateLogisticRegression"’s per-fold
ClassificationMetrics (Jube.Service.Agent.AdvancedAnalyticsTools.ModelEvaluation) carries the same four counts plus
Accuracy, Specificity and BalancedAccuracy directly. For the visual rendering of those same four counts,
Jube.Charting
Chart.ConfusionMatrix().Binary(trueNegative, falsePositive, falseNegative, truePositive).Rates().Render()
(or .FromPredictions(actual, predicted) straight from two label vectors) replaces CrossTable()’s printed layout with
row/column/overall percentages on hover and accuracy in the description — never ggplot2 or base R graphics.
Procedure 7: Output Logistic Regression Model as Probability
A logistic regression’s raw score converts to a probability with P = exp(score) / (1 + exp(score)), applied vectorised
over the whole prediction column.
PAutomaticLogisticRegression <- exp(AutomaticLogisticRegression) / (1 + exp(AutomaticLogisticRegression))
head(PAutomaticLogisticRegression)
FraudRisk <- mutate(FraudRisk, PAutomaticLogisticRegression)
Console output:
1 2 3 4 5 6
0.5137733 0.3045922 0.3045922 0.3045922 0.3045922 0.5137733
Tool equivalent:
Jube.Service.Agent.AdvancedAnalyticsTools.RelationshipsAndRegression.GeneralisedLinearModelTools
/ tool "ApplyLinkFunction" (link Logit, inverse: true) applies the same logistic function generally, for any link,
with out-of-domain values reported through InvalidCount instead of silently producing
NaN. In practice this manual conversion is rarely needed at all: "TrainLogisticRegression" and
"RecallLogisticRegression" already return a probability directly — the log-odds score never has to leave the model and
be converted back by hand.
Procedure 8: Creating a ROC Curve
The ROCR package appraises a classifier by comparing a probability prediction against the actual outcome across every
possible threshold at once. prediction() builds the comparison object;
performance() with measure = "tpr" and x.measure = "fpr" turns it into true-positive-rate against
false-positive-rate, plotted with base graphics.
library(ROCR)
ROCRPredictions <- prediction(FraudRisk$PAutomaticLogisticRegression, FraudRisk$Dependent)
ROCRPerformance <- performance(ROCRPredictions, measure = "tpr", x.measure = "fpr")
png("/work/roc.png")
plot(ROCRPerformance)
invisible(dev.off())
cat("ROC plot written\n")
Console output:
ROC plot written
AdvancedAnalytics equivalent: Jube.Charting, Chart.Roc() — the same RocChartBuilder named in Procedure 3.
Chart.Roc().FromScores("Logistic regression", actual, FraudRisk.PAutomaticLogisticRegression).Render()
replaces the prediction()/performance()/plot() pipeline with one call: it computes the curve (CurveMath.Roc,
ties handled, thinned to at most 2,000 points), draws the chance diagonal, marks the best cut-off by Youden’s J, and
puts the AUC straight in the legend rather than requiring Procedure 9’s separate performance(..., measure = "auc")
call — never ROCR’s base-graphics plot() in the product.
Procedure 9: Grading the ROC Performance with AUC
The Area Under Curve statistic reduces the ROC curve’s shape to one number between 0.5 (no better than chance) and 1
(perfect separation), read off the same performance() object with measure = "auc".
AUC <- performance(ROCRPredictions, measure = "auc")
AUC
AUC@y.values
Console output:
A performance instance
'Area under the ROC curve'
[[1]]
[1] 0.8034911
By the module’s own grading rubric (A: Outstanding > 0.9, B: Excellent > 0.8 and <= 0.9, C: Acceptable > 0.7 and <= 0.8, D: Poor > 0.6 and <= 0.7, E: Junk > 0.5 and <= 0.6) this model’s 0.803 grades B, excellent.
Tool equivalent: RocAuc (with RocAucLower95/RocAucUpper95, a 95% interval performance()
does not give at all) is a plain field on both the LogisticSplitMetrics and ClassificationMetrics
results named in Procedure 6’s mapping — returned automatically alongside every other held-out metric the moment
"TrainLogisticRegression" or "CrossValidateLogisticRegression" runs, never a second call. There is no tool-layer
equivalent for the literal A–E letter-grade bucketing: the product reports the continuous statistic and its confidence
interval, plus a plain-language comparison against the training-set AUC in
ClassifierEvaluationResult.Interpretation, and leaves the qualitative label to the reader.
