You defined C as an ordered variable, which it is, so logistf
automatically converts C to an orthogonal polynomial representation, which makes things harder to interpret. Why does R do this?
Orthogonal polynomial encoding is a form of trend analysis in that it is looking for the linear, quadratic and cubic trends in the effect of a categorical variable. Here you only have three values of C, so we can only do linear and cubic. This type of coding system should be used only with an ordinal variable in which the levels are equally spaced (which may or may not be the case in your data, the coding is evenly spaced, but the underlying variables may not be). In R it is not necessary to compute these values since this contrast can be obtained for any categorical variable by using the contr.poly
function that I asked you to use. This the default contrast used for ordered factor variables. This makes it easier to find a functional form that will allow you to model your categorical variables as if they were more continuous. This can help impose more structure on the effects, compared to something more flexible, where every category can have its own effect.
The fact that C.L coefficient is positive and significant, tells you that the data is consistent with a linear trend of C on the log odds: each time C goes up by 1, log odds go up by the same amount. The fact that quadratic is negative, means you have a deceleration in that trend (slower growth), but it is not significant.
Here is a reproducible example with some comments interspersed (reproducible code will be below):
> library(logistf)
> library(boot)
>
> # Calculate your OP model predictions for C=1,2,3
> inv.logit(-0.05228334 + 1.60842992*( -7.071068e-01) +
(-0.26863239)*0.4082483)
[1] 0.2142856
> inv.logit(-0.05228334 + 1.60842992*(-7.850462e-17) +
(-0.26863239)*-0.8164966)
[1] 0.5416667
> inv.logit(-0.05228334 + 1.60842992*(7.071068e-01) +
(-0.26863239)*0.4082483)
[1] 0.7261905
As you can see, this matches 2 of the 3 predictions that you reported. Now for the toy example:
> # Toy Example
> raw <- 'A C cL cQ
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 3 .7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 3 .7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 3 .7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 3 .7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 3 .7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 1 -.7071068 .4082483
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 2 -7.85e-17 -.8164966
+ 0 3 .7071068 .4082483
+ 1 1 -.7071068 .4082483
+ 1 2 -7.85e-17 -.8164966
+ 1 3 .7071068 .4082483
+ 1 2 -7.85e-17 -.8164966
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 2 -7.85e-17 -.8164966
+ 1 2 -7.85e-17 -.8164966
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 1 -.7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 1 -.7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 1 -.7071068 .4082483
+ 1 2 -7.85e-17 -.8164966
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 3 .7071068 .4082483
+ 1 1 -.7071068 .4082483
+ '
> Data <- read.table(text = raw, header = TRUE)
> Data$C <- factor(Data$C, levels = c(1, 2, 3), ordered = TRUE)
>
> # Show the orthogonal polynomial representation of C
> unique(Data[,c("C","cL","cQ")])
C cL cQ
1 2 -7.850000e-17 -0.8164966
2 1 -7.071068e-01 0.4082483
7 3 7.071068e-01 0.4082483
> contr.poly(3)
.L .Q
[1,] -7.071068e-01 0.4082483
[2,] -7.850462e-17 -0.8164966
[3,] 7.071068e-01 0.4082483
As you can see, the values returned by contr.poly(3)
match what is in my data. We will fit the model, but not a Firth logit, just a normal logit (but that should not really matter; also not clear to me what you are doing at home):
> # Plain Logit Model with Orthogonal Polynomial
> op_model<-logistf(A~C, family=binomial(link="logit"), data=Data,
firth=FALSE, pl=TRUE)
> summary(op_model)
logistf(formula = A ~ C, data = Data, pl = TRUE, firth = FALSE,
family = binomial(link = "logit"))
Model fitted by Standard ML
Confidence intervals and p-values by Profile Likelihood Profile Likelihood Profile Likelihood
coef se(coef) lower 0.95 upper 0.95 Chisq p
(Intercept) -0.9098647 0.2733188 -1.4739880 -0.3932878 12.372954 0.0004355978
C.L 1.1091790 0.4580922 0.2432744 2.0621977 6.387326 0.0114938060
C.Q 0.5206835 0.4882321 -0.4030296 1.5437748 1.191198 0.2750880661
Likelihood ratio test=7.786674 on 2 df, p=0.02037724, n=74
Wald test = 7.538426 on 2 df, p = 0.02307021
Covariance-Matrix:
[,1] [,2] [,3]
[1,] 0.07470317 -0.01678486 -0.01164408
[2,] -0.01678486 0.20984848 -0.02055717
[3,] -0.01164408 -0.02055717 0.23837055
You can even see what R is actually doing to C with this bit of code:
> head(model.matrix(logistf(Data$A~Data$C, family=binomial(link="logit"), firth=FALSE, pl=TRUE)))
(Intercept) Data$C.L Data$C.Q
1 1 -7.850462e-17 -0.8164966
2 1 -7.071068e-01 0.4082483
3 1 -7.850462e-17 -0.8164966
4 1 -7.850462e-17 -0.8164966
5 1 -7.071068e-01 0.4082483
6 1 -7.071068e-01 0.4082483
Now we will do the same thing using the cL and cQ variables I entered myself. We should see the same model summary as above:
> # Plain Logit Model with Orthogonal Polynomial by Hand
> op_model_by_hand<-logistf(A~cL+cQ, family=binomial(link="logit"),
data=Data, firth=FALSE, pl=TRUE)
> summary(op_model_by_hand)
logistf(formula = A ~ cL + cQ, data = Data, pl = TRUE, firth = FALSE,
family = binomial(link = "logit"))
Model fitted by Standard ML
Confidence intervals and p-values by Profile Likelihood Profile Likelihood Profile Likelihood
coef se(coef) lower 0.95 upper 0.95 Chisq p
(Intercept) -0.9098647 0.2733188 -1.4739880 -0.3932878 12.372954 0.0004355978
cL 1.1091789 0.4580922 0.2432744 2.0621977 6.387326 0.0114938060
cQ 0.5206835 0.4882321 -0.4030296 1.5437748 1.191198 0.2750880661
Likelihood ratio test=7.786674 on 2 df, p=0.02037724, n=74
Wald test = 7.538426 on 2 df, p = 0.02307021
Covariance-Matrix:
[,1] [,2] [,3]
[1,] 0.07470317 -0.01678486 -0.01164408
[2,] -0.01678486 0.20984847 -0.02055717
[3,] -0.01164408 -0.02055717 0.23837054
This way to represent C is somewhat unintuitive. The specific values of cL and cQ themselves have no meaning to us because they were computed by R to make all the contrasts linearly independent of one another. This kind of encoding is just one of at least 9 possible ways to encode categorical variables. For OP, it probably does not make too much sense to think hard about the intercept insignificance. And it certainly does not mean that you should take it out. You should never decide which variables to omit from the model based on statistical significance only.
This is how you might do the one-hot encoding that lends itself to easier interpretation:
> # Remove the constraint that C is ordered to get one-hot encoded version
> Data$C <- factor(Data$C, levels = c(1, 2,3), ordered = FALSE)
> oh_model<-logistf(A~C, family=binomial(link="logit"), data=Data,
firth=FALSE, pl=TRUE)
> summary(oh_model)
logistf(formula = A ~ C, data = Data, pl = TRUE, firth = FALSE,
family = binomial(link = "logit"))
Model fitted by Standard ML
Confidence intervals and p-values by Profile Likelihood Profile Likelihood Profile Likelihood
coef se(coef) lower 0.95 upper 0.95 Chisq p
(Intercept) -1.4816045 0.4954337 -2.5755111 -0.5899917 11.55500406 0.0006756716
C2 0.1466035 0.7057522 -1.2656065 1.5604230 0.04313599 0.8354694171
C3 1.5686159 0.6478402 0.3440419 2.9163880 6.38732576 0.0114938060
Likelihood ratio test=7.786674 on 2 df, p=0.02037724, n=74
Wald test = 7.538426 on 2 df, p = 0.02307021
Covariance-Matrix:
[,1] [,2] [,3]
[1,] 0.2454545 -0.2454545 -0.2454545
[2,] -0.2454545 0.4980861 0.2454545
[3,] -0.2454545 0.2454545 0.4196970
Personally, this is the model I would use myself with your data. You can use it to calculate the baseline odds from the intercept, and the exponentiated coefficients will give you the multiplicative effect on those baseline odds.
I don't think there is any way to interpret the OP model in the same way. I also don't think it makes sense to treat C as an ordered variable here and try to find trends when it only has three values.
Code:
library(logistf)
library(boot)
# Calculate your OP model predictions for C=1,2,3
inv.logit(-0.05228334 + 1.60842992*( -7.071068e-01) + (-0.26863239)*0.4082483)
inv.logit(-0.05228334 + 1.60842992*(-7.850462e-17) + (-0.26863239)*-0.8164966)
inv.logit(-0.05228334 + 1.60842992*(7.071068e-01) + (-0.26863239)*0.4082483)
# Toy Example
raw <- 'A C cL cQ
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 3 .7071068 .4082483
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 3 .7071068 .4082483
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 3 .7071068 .4082483
0 2 -7.85e-17 -.8164966
0 3 .7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 3 .7071068 .4082483
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 3 .7071068 .4082483
0 1 -.7071068 .4082483
0 3 .7071068 .4082483
0 3 .7071068 .4082483
0 3 .7071068 .4082483
0 3 .7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 1 -.7071068 .4082483
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 2 -7.85e-17 -.8164966
0 3 .7071068 .4082483
1 1 -.7071068 .4082483
1 2 -7.85e-17 -.8164966
1 3 .7071068 .4082483
1 2 -7.85e-17 -.8164966
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 2 -7.85e-17 -.8164966
1 2 -7.85e-17 -.8164966
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 1 -.7071068 .4082483
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 1 -.7071068 .4082483
1 3 .7071068 .4082483
1 1 -.7071068 .4082483
1 2 -7.85e-17 -.8164966
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 3 .7071068 .4082483
1 1 -.7071068 .4082483
'
Data <- read.table(text = raw, header = TRUE)
Data$C <- factor(Data$C, levels = c(1, 2, 3), ordered = TRUE)
# Show the orthogonal polynomial representation of C
unique(Data[,c("C","cL","cQ")])
contr.poly(3)
# Plain Logit Model with Orthogonal Polynomial
op_model<-logistf(A~C, family=binomial(link="logit"), data=Data, firth=FALSE, pl=TRUE)
summary(op_model)
head(model.matrix(logistf(Data$A~Data$C, family=binomial(link="logit"), firth=FALSE, pl=TRUE)))
# Plain Logit Model with Orthogonal Polynomial by Hand
op_model_by_hand<-logistf(A~cL+cQ, family=binomial(link="logit"), data=Data, firth=FALSE, pl=TRUE)
summary(op_model_by_hand)
# Remove the constraint that C is ordered to get one-hot encoded version
Data$C <- factor(Data$C, levels = c(1, 2,3), ordered = FALSE)
oh_model<-logistf(A~C, family=binomial(link="logit"), data=Data, firth=FALSE, pl=TRUE)
summary(oh_model)