The problem here is twofold:
Carry == none is completely aliased with Period == 1: this makes your model of less than full rank and is likely what prompted you to include singular.ok=TRUE in the ANOVA call. Really only one of the two remaining Carry values contributes any information because they are also aliased with the remaining periods.
- When provided with such model
car::Anova changes its default behaviour, as indicated by the message that comes out of your call:
Note: model has aliased coefficients
sums of squares computed by model comparison
In practice this message means that they swap to car::Anova.glm which in this case calls the internal car:::Anova_III_F_glm method. That in turn performs inference on individual terms via stats::drop1 (see here).
Now, the problem is that when Period is dropped from the model, Carry is no longer aliased with it. An extra coefficient just became estimable. Because drop1 calculates the degrees of freedom by comparing the ranks of the full and reduced model, and this last one suddenly is of full rank, it is off by one. You can see this behaviour using just drop1 or by inspecting the models:
## Full model (car::Anova will convert to glm if not of full rank)
glm <- glm(y ~ Subject + Treat + Period + Carry, data=bioequiv)
coef(glm)[37:41]
#> TreatB Period2 Period3 CarryB Carrynone
#> 9.5940 -7.7646 -6.3104 7.6397 NA ## Aliased
drop1(glm, "Period")
#> Df Deviance AIC
#> Period 1 35810 1013.3 ## Df=1?
## Reduced model
glm0 <- glm(y ~ Subject + Treat + Carry, data=bioequiv)
coef(glm0)[37:39]
#> TreatB CarryB Carrynone
#> 9.5940 7.6397 7.0375 ## No longer aliased!
glm$rank - glm0$rank
#> 1 ## Oops... expected 2
Is this a bug? Maybe? I don't see anything in drop1's documentation about this behaviour, but on the other hand it's not exactly good form to be working with such less-than-full-rank models... The real solution is probably to parametrize the Carry effect as a single dummy for any of the levels other than none. This will give consistent results, for example:
contrasts <- list(Subject=contr.sum, Period=contr.sum, Treat=contr.sum)
df <- within(bioequiv, {CB <- 0+(Carry=="B")})
lm <- lm(y~Subject + Period + Treat + CB, data=df, contrasts=contrasts)
car::Anova(lm, type="III")
#> Sum Sq Df F value Pr(>F)
#> (Intercept) 633029 1 1203.3274 <2e-16 ***
#> Subject 403586 35 21.9194 <2e-16 ***
#> Period 930 2 0.8835 0.4180
#> ...
## n.b. car did not use stats::drop1() in the above Anova call
drop1(lm, "Period")
#> Df Sum of Sq RSS AIC
#> Period 2 929.54 36702 705.47 ## Matches