frscore 0.3.1
A new version of frscore
is now on
CRAN.
Alas, this version does not deliver the features I promised in an
earlier post. Other issues were simply
deemed more urgent at this time. The
NEWS file
gives a bullet-point summary of changes. This post is an attempt to give
an overview of a conceptual issue that necessitated the
biggest change in the package. Due to this change, some old results generated with frscored_cna()
and frscore()
may not be reproducible with the new default behaviors, hence this post. Other developments are
explained at the end of the post.
Measuring agreement in causal relevance ascriptions
In
Parkkinen & Baumgartner
(2021) we describe the
conceptual background of the procedure implemented in the frscore
functions. The idea is that for a cna
or other CCM model, robustness
against changes in consistency and coverage settings need not mean that
the very same model is inferred at many different settings. Rather, what
matters is whether or not the causal ascriptions made by a model are in
agreement with (many) other models inferred at different settings. In
practice, the frscore
functions implement this so that models inferred
at different con/cov settings are checked against each other with
cna::is.submodel()
, counting the number of sub- and supermodels each
model has in the whole lot. This number is each model’s unadjusted
fit-robustness score.
The aforementioned paper and the manual page for cna::is.submodel()
both give wordy definitions of the submodel relation, but in short, each
atomic component model (“asf”) of a submodel must have a counterpart asf
in the supermodel such that the former could be generated by eliminating
symbols from the latter. E.g. the single-asf model A+B<->C
is a
submodel of A*Y+B<->C
, as we can get to the former by removing *Y
from the latter. To be specific, we might call this relation a
syntactic submodel relation, as it involves no interpretation, causal
or otherwise, of the meaning of the symbols that appear in the models.
When we interpret the models causally, a syntactic submodel relation
means that all claims about direct causal relevance made by the
submodel are contained in the supermodel. This is because claims of
direct causation are explicitly represented by the asf syntax: anything
on the left hand side of the equivalence sign "<->"
is a direct cause of the outcome on the
right hand side.
In the context of frscore
, the problem with this is twofold. First,
complex models comprising more than one asf can make indirect causal
relevance ascriptions that cannot be read off the overt syntax of the
model. Second, the concept of direct causation is relative to (the set
of factors included in) a model. For example, (A+B<->C)*(C+D<->E)
claims that A
and B
are indirectly causally relevant for E
in
virtue of causing C
. Since every causal relevance claim corresponds to
a claim about difference-making, this means that the model claims that
there are circumstances in which wiggling A
while suppressing B
, or
the other way around, makes a difference to E
(via making a difference
to C
). Now think of A+B<->E
. A+B<->E
describes those same causal
relations between {A,B,E}
, entailing the exact same difference-making
relations between those three factors, but does so at a coarser
granularity, omitting the mediating factor C
. While
(A+B<->C)*(C+D<->E)
gives a more detailed description of the causal
process than A+B<->E
, the latter is clearly not making claims about
causal relevance that conflict the more detailed model. Hence, if these
were models inferred from the same data set, the frscore
functions
should recognize that they agree in their causal relevance ascriptions.
But when models are compared using cna::is.submodel()
, this is not
recognized: A+B<->E
is not a syntactic submodel of
(A+B<->C)*(C+D<->E)
, because there is no suitable counterpart asf in
the latter for A+B<->E
.
Consider another pair of models: (A+B*D<->C)*(C<->G)
and
(A+B*D<->C)*(C+D<->G)
. Here, the former model is a syntactic
submodel of the latter. Yet, the former makes a causal relevance claim
that disagrees with the latter. (A+B*D<->C)*(C<->G)
claims that A
,
B
and D
are indirectly relevant for G
in virtue of causing C
.
The latter model claims that A
is indirectly relevant for G
, D
directly so, and B
not causally relevant for G
at all. It may be
easier to make this conflict obvious by considering the associated
difference-making claims. (A+B*D<->C)*(C<->G)
claims that
wiggling B
makes a difference to G
when D
is present and A
is
absent. By contrast, (A+B*D<->C)*(C+D<->G)
claims that this is
impossible: According to (A+B*D<->C)*(C+D<->G)
, G
is always present
whenever D
is. Yet, the old frscore
functions that relied on
cna::is.submodel()
would judge this pair of models to be free of
conflicting causal relevance ascriptions.
Causal submodels
To fix this problem, frscore 0.3.1
introduces a new function
causal_submodel()
that is like cna::is.submodel()
, except that it
checks whether all causal relevance ascriptions, direct or indirect, of
one model have a suitable[1] counterpart in another model. If yes, one
is a causal submodel of the other. The syntax of causal_submodel()
resembles cna::is.submodel()
: the candidate submodel to be tested is
given as the first argument, and the target supermodel as the second. A
notable difference is that unlike cna::is.submodel()
, you cannot (for
now) give causal_submodel()
a vector of more than one candidate
models at a time. For processing multi-valued models,
causal_submodel()
will also need to know the range of admissible
factor values. This is provided via the argument dat
, and would
typically be the data set from which the models were inferred.
By default, frscored_cna()
and frscore()
will now use
causal_submodel()
to compare models when calculating fr-scores. This
induces a slight performance penalty compared to the older versions of
these functions. In case performance is crucial, or one wishes to
replicate old results, frscored_cna()
and frscore()
functions have a
new argument comp.method
that allows the user to switch to the old
behavior. comp.method
takes values "causal_submodel"
(default) or
"is.submodel"
. With the default value, fr-scores are counted using
causal_submodel()
, and with comp.method = "is.submodel"
,
cna::is.submodel()
[2] is used instead. According to our tests, the
differences in the results are small between these two options, so using
comp.method = "is.submodel"
might still be reasonable for some users.
We nonetheless recommend using the default value when possible, as this
more closely tracks the kind of robustness we intended.
When processing multi-valued models with frscore()
using
comp.method = "causal_submodel"
, the admissible factor value range
must be provided as the (new) argument dat
, similarly to
causal_submodel()
. In frscored_cna()
, the user-supplied data set is
automatically used for this purpose.
Below are some examples of causal_submodel()
output compared to
cna::is.submodel()
.
if(compareVersion(as.character(packageVersion("frscore")), "0.3.1") > 0){
install.packages("frscore") # update package if needed
}
library(frscore)
## Loading required package: cna
## Registered S3 method overwritten by 'cna':
## method from
## some.data.frame car
## Loading required package: lifecycle
target <- "(A+B<->C)*(C+D<->E)"
candidate <- "A+B<->E"
is.submodel(x = candidate, y = target)
## A+B<->E
## FALSE
## attr(,"target")
## [1] "(A+B<->C)*(C+D<->E)"
causal_submodel(x = candidate, y = target)
## A+B<->E
## TRUE
## attr(,"target")
## [1] "(A+B<->C)*(C+D<->E)"
target2 <- "(A+B*D<->C)*(C+D<->G)"
candidate2 <- "(A+B*D<->C)*(C<->G)"
is.submodel(candidate2, target2)
## (A+B*D<->C)*(C<->G)
## TRUE
## attr(,"target")
## [1] "(A+B*D<->C)*(C+D<->G)"
causal_submodel(candidate2, target2)
## (A+B*D<->C)*(C<->G)
## FALSE
## attr(,"target")
## [1] "(A+B*D<->C)*(C+D<->G)"
# two models inferred from the `d.pban` example data set
mv_target <- "(C=1 + C=2 + F=2 <-> PB=1)*(F=1 + F=2 <-> T=2)"
mv_candidate <- "(T=1 + V=0 <-> PB=1)*(F=1 + F=2 <-> T=2)"
# causal_submodel() needs the data to process mv models
causal_submodel(x = mv_candidate, y = mv_target, dat = d.pban)
## (T=1+V=0<->PB=1)*(F=1+F=2<->T=2)
## FALSE
## attr(,"target")
## [1] "(C=1+C=2+F=2<->PB=1)*(F=1+F=2<->T=2)"
causal_submodel()
is an approximation of a strictly speaking valid test
for containment of causal relevance claims between models, even though a
closer approximation than cna::is.submodel()
. Internally, the inputted
models need to be manipulated in various ways to explicate all indirect
causal ascriptions, which involves minimizing the manipulated models to
remove redundancies. To speed things up, the minimization relies on
cna::rreduce()
, which randomly chooses a single minimization path
whenever faced with ambiguous models. In practice, this may result in
the output of causal_submodel()
changing between subsequent calls on
the same pair of models, if those models happen to be ambiguous. This
however happens rarely enought to not affect the frscore
functions in
a meaningful way. Models that describe causal feedback cycles present
another unique problem. Such models claim that some factors are causally
relevant for themselves, as in (A+B<->C)*(C+D<->A)
. It is not conceptually entirely clear how comparisons between cyclic and non-cyclic models should be conducted. Here causal_submodel()
takes
the least costly option and simply checks whether a syntactic submodel
relation is present between the candidate and the target models, and returns the result.
Other changes
The scoretype
argument of frscore()
and frscored_cna()
is now
deprecated, and will be removed in the next version of the package. The
reason for this is simple: If you changed the scoretype
value from its
default, thereby forcing the scores to be counted based on either sub-
or supermodel relations only, then the scores would not reflect the
robustness of the models as we defined it in
Parkkinen & Baumgartner
(2021). We can offer no
guidance as to how such results should be interpreted, so having the
option to change how the scores are calculated can only lead to
confusion. If one is mostly interested in complex (resp. simple) models,
one should explicitly argue for one’s choice of focus, rather than
changing the fr-scoring routine to arbitrarily favor one’s favorite
model(s). After running the whole fr-scoring routine with
frscored_cna()
, one can always view all the models returned in a
reanalysis series, and the score composition for every model type, by
inspecting the respective elements in the return object. From there, one
can calculate any other statistics about the returned models one wishes.
The ncsf
argument in rean_cna()
is deprecated in favor of n.init
,
which does exactly the same thing: its value determines the maximum
number of complex models (“csf”s) computed. Note that this only applies
when output = "csf"
(the default). In practice, rean_cna()
is primarily a helper function called by frscored_cna()
for performing a reanalysis series; I’d imagine it to be very rare for a regular user to need it directly. Hence, n.init
is also added as a new
explicit argument to frscored_cna()
, to make it obvious to the user that one can control the maximum number of csfs if needed, and to make eventual warnings about n.init
value that actually arise from cna::csf()
less confusing. This
change brings the syntax of both of these functions in line with the cna
functions that they rely on anyway. The default value of n.init
is
also the same as in the relevant cna
functions. As the cna
functions have become more efficient, imposing a lower limit is usually
no longer needed.
frscore()
gains an argument dat
, which was covered above.
[1] See the manual page for causal_submodel()
for an explanation of
what counts as a “suitable” counterpart.
[2] To be precise, the package now uses a faster, internal
implementation of cna::is.submodel
, thanks to generous help of cna
maintainer Mathias Ambühl.
- Posted on:
- March 5, 2023
- Length:
- 9 minute read, 1827 words
- Categories:
- frscore
- Tags:
- frscore
- See Also:
- Visualizing frscore results, part 1