Repository: monarch-initiative/gpsea (language: python)

Issue:
Improve AR predicate defaults
The `autosomal_recessive` predicate needs the following changes:

* use *all* variants (effectively drop the variant predicate option)
* compare individuals with AC=1 vs. AC=2 (effectively drop the `partitions` option)
* rename to `allele_count`

@pnrobinson please give this a :+1: if sounds OK or let me know if it does not. Thank you!


Discussion / hints:

Fix patch:
diff --git a/docs/user-guide/analyses/phenotype-groups.rst b/docs/user-guide/analyses/phenotype-groups.rst
index 21300a91..6240f107 100644
--- a/docs/user-guide/analyses/phenotype-groups.rst
+++ b/docs/user-guide/analyses/phenotype-groups.rst
@@ -106,42 +106,42 @@ Genotype predicate
 ------------------
 
 We want to separate the patients into two groups: a group *with* a frameshift variant
-and a group *without* a frameshift variant, based on the functional annotation.
+and a group *without* a frameshift variant (i.e. any other heterozygous variant).
 We will use the *MANE* transcript for the analysis:
 
+>>> tx_id = 'NM_181486.4'
+
 Building a genotype predicate is a two step process. 
 First, we create a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate`
-to test if the variant leads to a frameshift (in this case):
+to test if the variant is predicted to lead to a frameshift in `NM_181486.4`:
 
 >>> from gpsea.model import VariantEffect
 >>> from gpsea.analysis.predicate.genotype import VariantPredicates
->>> tx_id = 'NM_181486.4'
 >>> is_frameshift = VariantPredicates.variant_effect(VariantEffect.FRAMESHIFT_VARIANT, tx_id)
 >>> is_frameshift.get_question()
 'FRAMESHIFT_VARIANT on NM_181486.4'
 
-and then we choose the expected mode of inheritance to test. In case of *TBX5*,
-we expect the autosomal dominant mode of inheritance:
+and then we wrap `is_frameshift` in a :class:`~gpsea.analysis.predicate.genotype.monoallelic_predicate` 
+to classify each *TBX5* cohort member either as an individual with one frameshift allele (`Frameshift`)
+or as an idividual with one non-frameshift allele (`Other`):
 
->>> from gpsea.analysis.predicate.genotype import autosomal_dominant
->>> gt_predicate = autosomal_dominant(is_frameshift)
+>>> from gpsea.analysis.predicate.genotype import monoallelic_predicate
+>>> gt_predicate = monoallelic_predicate(
+...     a_predicate=is_frameshift,
+...     a_label="Frameshift",
+...     b_label="Other",
+... )
 >>> gt_predicate.display_question()
-'What is the genotype group: No allele, Monoallelic'
-
-`gt_predicate` will assign the patients with no frameshift variant allele into `No allele` group
-and the patients with one frameshift allele will be assigned into `Monoallelic` group.
-Note, any patient with 2 or more alleles will be *omitted* from the analysis.
-
-.. note::
+'Allele group: Frameshift, Other'
 
-   Mode of inheritance testing is not the only way to dissect by a genotype.
-   See the :ref:`genotype-predicates` section for more info.
+In the subsequent analysis, `gt_predicate` will assign a cohort member into the respective group.
+Note, any patient with :math:`0` or :math:`\ge 2` alleles will be *omitted* from the analysis.
 
 
 Phenotype predicates
 --------------------
 
-We recommend testing the genotype phenotype association for all HPO terms that are present in 2 or more cohort members,
+We recommend testing the genotype phenotype association for all HPO terms that annotate the cohort members,
 while taking advantage of the HPO graph structure and of the :ref:`true-path-rule`.
 We will use the :func:`~gpsea.analysis.predicate.phenotype.prepare_predicates_for_terms_of_interest`
 utility function to generate phenotype predicates for all HPO terms.
@@ -170,7 +170,7 @@ including the *indirect* annotations whose presence is implied by the :ref:`true
 Statistical test
 ----------------
 
-We will use :ref:<fisher-exact-test> to test the association
+We will use :ref:`fisher-exact-test` to test the association
 between genotype and phenotype groups, as described previously.
 
 >>> from gpsea.analysis.pcats.stats import FisherExactTest
@@ -182,10 +182,10 @@ FET will compute a p value for each genotype phenotype group.
 Multiple testing correction
 ---------------------------
 
-In the case of this cohort, we could test association between having a frameshift variant and one of 260 HPO terms.
+In the case of this cohort, we could test association between having a frameshift variant and one of 369 HPO terms.
 However, testing multiple hypotheses on the same dataset increases the risk of finding a significant association
 by chance.
-GPSEA uses a two-pronged strategy to mitigate this risk - use Phenotype MTC filter and multiple testing correction.
+GPSEA uses a two-pronged strategy to mitigate this risk - a phenotype MTC filter and multiple testing correction.
 
 .. note::
 
@@ -270,10 +270,10 @@ ordered by the corrected p value (Benjamini-Hochberg FDR):
 
 
 The table shows that several HPO terms are significantly associated
-with presence of a heterozygous (`Monoallelic`) frameshift variant in *TBX5*.
+with presence of a heterozygous (`Frameshift`) frameshift variant in *TBX5*.
 For example, `Ventricular septal defect <https://hpo.jax.org/browse/term/HP:0001629>`_
-was observed in 42/71 (59%) patients with no frameshift allele (`No allele`)
-but it was observed in 19/19 (100%) patients with a frameshift allele (`Monoallelic`).
+was observed in 42/71 (59%) patients with no frameshift allele (`Other`)
+but it was observed in 19/19 (100%) patients with a frameshift allele (`Frameshift`).
 Fisher exact test computed a p value of `~0.000242`
 and the p value corrected by Benjamini-Hochberg procedure
 is `~0.005806`.
diff --git a/docs/user-guide/analyses/report/tbx5_frameshift.csv b/docs/user-guide/analyses/report/tbx5_frameshift.csv
index f02767d1..a9c035a6 100644
--- a/docs/user-guide/analyses/report/tbx5_frameshift.csv
+++ b/docs/user-guide/analyses/report/tbx5_frameshift.csv
@@ -1,26 +1,26 @@
-What is the genotype group,No allele,No allele,Monoallelic,Monoallelic,,
+Allele group,Frameshift,Frameshift,Other,Other,,
 ,Count,Percent,Count,Percent,Corrected p values,p values
-Ventricular septal defect [HP:0001629],42/71,59%,19/19,100%,0.005806240832840839,0.00024192670136836825
-Absent thumb [HP:0009777],18/100,18%,14/31,45%,0.044564058192239124,0.003713671516019927
-Secundum atrial septal defect [HP:0001684],23/55,42%,4/22,18%,0.4065940176561687,0.06544319142266644
-Triphalangeal thumb [HP:0001199],23/99,23%,13/32,41%,0.4065940176561687,0.06932119159387057
-Muscular ventricular septal defect [HP:0011623],8/84,10%,6/25,24%,0.4065940176561687,0.08470708701170182
-Short thumb [HP:0009778],25/69,36%,8/30,27%,1.0,0.48700997145537483
-Absent radius [HP:0003974],9/43,21%,6/25,24%,1.0,0.7703831604944444
-Abnormal cardiac septum morphology [HP:0001671],89/89,100%,28/28,100%,1.0,1.0
-Abnormal hand morphology [HP:0005922],75/75,100%,20/20,100%,1.0,1.0
-Atrial septal defect [HP:0001631],63/65,97%,20/20,100%,1.0,1.0
-Abnormal atrial septum morphology [HP:0011994],64/64,100%,20/20,100%,1.0,1.0
-Abnormal cardiac atrium morphology [HP:0005120],64/64,100%,20/20,100%,1.0,1.0
-Hypoplasia of the radius [HP:0002984],34/75,45%,6/14,43%,1.0,1.0
-Abnormal appendicular skeleton morphology [HP:0011844],93/93,100%,34/34,100%,1.0,1.0
-Aplasia/hypoplasia of the extremities [HP:0009815],78/78,100%,22/22,100%,1.0,1.0
-Aplasia/hypoplasia involving the skeleton [HP:0009115],80/80,100%,23/23,100%,1.0,1.0
-Aplasia/hypoplasia involving bones of the upper limbs [HP:0006496],78/78,100%,22/22,100%,1.0,1.0
-Aplasia/hypoplasia involving bones of the extremities [HP:0045060],78/78,100%,22/22,100%,1.0,1.0
-Abnormal long bone morphology [HP:0011314],50/50,100%,13/13,100%,1.0,1.0
-Aplasia/Hypoplasia of fingers [HP:0006265],44/44,100%,19/19,100%,1.0,1.0
-Aplasia/hypoplasia involving bones of the hand [HP:0005927],44/44,100%,19/19,100%,1.0,1.0
-Abnormal finger morphology [HP:0001167],64/64,100%,31/31,100%,1.0,1.0
-Abnormal digit morphology [HP:0011297],67/67,100%,33/33,100%,1.0,1.0
-Abnormal thumb morphology [HP:0001172],58/58,100%,31/31,100%,1.0,1.0
+Ventricular septal defect [HP:0001629],19/19,100%,42/71,59%,0.005806240832840839,0.00024192670136836828
+Absent thumb [HP:0009777],14/31,45%,18/100,18%,0.04456405819223913,0.0037136715160199273
+Secundum atrial septal defect [HP:0001684],4/22,18%,23/55,42%,0.4065940176561687,0.06544319142266644
+Triphalangeal thumb [HP:0001199],13/32,41%,23/99,23%,0.4065940176561687,0.06932119159387057
+Muscular ventricular septal defect [HP:0011623],6/25,24%,8/84,10%,0.4065940176561687,0.08470708701170182
+Short thumb [HP:0009778],8/30,27%,25/69,36%,1.0,0.4870099714553749
+Absent radius [HP:0003974],6/25,24%,9/43,21%,1.0,0.7703831604944444
+Atrial septal defect [HP:0001631],20/20,100%,63/65,97%,1.0,1.0
+Abnormal atrial septum morphology [HP:0011994],20/20,100%,64/64,100%,1.0,1.0
+Abnormal cardiac septum morphology [HP:0001671],28/28,100%,89/89,100%,1.0,1.0
+Abnormal cardiac atrium morphology [HP:0005120],20/20,100%,64/64,100%,1.0,1.0
+Hypoplasia of the radius [HP:0002984],6/14,43%,34/75,45%,1.0,1.0
+Abnormal appendicular skeleton morphology [HP:0011844],34/34,100%,93/93,100%,1.0,1.0
+Aplasia/hypoplasia of the extremities [HP:0009815],22/22,100%,78/78,100%,1.0,1.0
+Aplasia/hypoplasia involving the skeleton [HP:0009115],23/23,100%,80/80,100%,1.0,1.0
+Aplasia/hypoplasia involving bones of the upper limbs [HP:0006496],22/22,100%,78/78,100%,1.0,1.0
+Aplasia/hypoplasia involving bones of the extremities [HP:0045060],22/22,100%,78/78,100%,1.0,1.0
+Abnormal long bone morphology [HP:0011314],13/13,100%,50/50,100%,1.0,1.0
+Abnormal hand morphology [HP:0005922],20/20,100%,75/75,100%,1.0,1.0
+Abnormal thumb morphology [HP:0001172],31/31,100%,58/58,100%,1.0,1.0
+Abnormal finger morphology [HP:0001167],31/31,100%,64/64,100%,1.0,1.0
+Abnormal digit morphology [HP:0011297],33/33,100%,67/67,100%,1.0,1.0
+Aplasia/Hypoplasia of fingers [HP:0006265],19/19,100%,44/44,100%,1.0,1.0
+Aplasia/hypoplasia involving bones of the hand [HP:0005927],19/19,100%,44/44,100%,1.0,1.0
diff --git a/docs/user-guide/predicates/allele_count.rst b/docs/user-guide/predicates/allele_count.rst
new file mode 100644
index 00000000..cd62be60
--- /dev/null
+++ b/docs/user-guide/predicates/allele_count.rst
@@ -0,0 +1,80 @@
+.. _allele-count:
+
+=====================
+Group by allele count
+=====================
+
+Sometimes, we may want to compare individuals with different allele counts of a single variant category.
+For instance, we may want to compare the survival of individuals harboring a mutation in *EGFR* (:math:`AC = 1`)
+with those with no such mutation (:math:`AC = 0`). 
+Alternatively, in some genes, heterozygous mutations (:math:`AC = 1`) and biallelic mutations (:math:`AC = 2`)
+can lead to different diseases.
+
+The allele count analysis differs from the :ref:`variant-category` analysis.
+The allele count analysis partitions the individuals based on different allele counts of one genotype category,
+while the variant category analysis partitions the individuals based on a fixed allele count of different genotype categories.
+
+The allele count analysis can be done with :func:`~gpsea.analysis.predicate.genotype.allele_count` predicate.
+
+********
+Examples
+********
+
+Compare the individuals with *EGFR* mutation
+============================================
+
+First, let's create a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate` to include
+any *EGFR* variant:
+
+>>> from gpsea.analysis.predicate.genotype import VariantPredicates
+>>> affects_egfr = VariantPredicates.gene(symbol="EGFR")
+>>> affects_egfr.get_question()
+'affects EGFR'
+
+Next, we create allele count predicate to partition the individuals
+based on presence of zero or one *EGFR* mutation allele:
+
+>>> from gpsea.analysis.predicate.genotype import allele_count
+>>> gt_predicate = allele_count(
+...     counts=({0,}, {1,}),
+...     target=affects_egfr,
+... )
+>>> gt_predicate.display_question()
+'Allele count: 0, 1'
+
+We create the predicate with two arguments.
+The `counts` argument takes a tuple of two sets, to partition the individuals
+based on zero (``{0,}``) or one (``{1,}``) target variant allele.
+The `target` takes a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate`
+for defining the target variants.
+
+We can use the `gt_predicate` to partition a cohort along the genotype axis,
+e.g. to compare the patient survivals in a `survival analysis <survival>`.
+
+
+Compare the individuals with monoallelic and biallelic mutations
+================================================================
+
+Let's prepare a predicate for grouping individuals based on one or two alleles of a target mutation.
+
+For this example, the target mutation is any mutation that affects *LMNA*
+
+>>> from gpsea.analysis.predicate.genotype import VariantPredicates
+>>> affects_lmna = VariantPredicates.gene(symbol="LMNA")
+>>> affects_lmna.get_question()
+'affects LMNA'
+
+and we will compare the individuals with one allele with those with two alleles:
+
+>>> gt_predicate = allele_count(
+...     counts=({1,}, {2,}),
+...     target=affects_lmna,
+... )
+>>> gt_predicate.display_question()
+'Allele count: 1, 2'
+
+The predicate will partition the individuals into two groups:
+those with one *LMNA* variant allele and those with two *LMNA* variant alleles.
+The individual with other allele counts (e.g. `0` or `3`) will be excluded
+from the analysis.
+
diff --git a/docs/user-guide/predicates/allele_count_predicates.rst b/docs/user-guide/predicates/allele_count_predicates.rst
deleted file mode 100644
index b358c481..00000000
--- a/docs/user-guide/predicates/allele_count_predicates.rst
+++ /dev/null
@@ -1,200 +0,0 @@
-.. _allele-count-predicates:
-
-=======================
-Allele count predicates
-=======================
-
-Sometimes we want to analyze the individuals who have the same allele count of the variants of interest.
-Most of the time, the allele count is `1` to analyze individuals with an autosomal dominant disease,
-or `2` for the individuals with a disease segregating with the autosomal recessive mode of inheritance.
-
-GPSEA provides :func:`~gpsea.analysis.predicate.genotype.monoallelic_predicate`
-and :func:`~gpsea.analysis.predicate.genotype.biallelic_predicate` to investigate
-the individuals with `1` or `2` alleles of the variants of interest.
-
-Both predicates take two variant predicates (:class:`~gpsea.analysis.predicate.genotype.VariantPredicate`)
-that are used to determine the allele count of the target variants. 
-
-
-.. _monoallelic-predicate:
-
-*********************
-Monoallelic predicate
-*********************
-
-Monoallelic predicate compares individuals who have *one* allele of a variant of interest.
-The predicate requires two variant predicates `A` and `B` for choosing
-the variants of interest (e.g. variants overlapping with a specific protein domain).
-The allele counts :math:`A_{AC}` and :math:`B_{AC}` for the variants of interest
-are computed and the individual is assigned into a category
-based on the following table:
-
-.. table:: Monoallelic predicate genotype categories
-
-    +-------------------+-------------------+-----------+
-    | :math:`A_{AC}`    | :math:`B_{AC}`    | Category  |
-    +===================+===================+===========+
-    | 1                 | 0                 | A         |
-    +-------------------+-------------------+-----------+
-    | 0                 | 1                 | B         |
-    +-------------------+-------------------+-----------+
-    | other             | other             | ``None``  |
-    +-------------------+-------------------+-----------+
-
-The individuals with different allele counts
-(e.g. :math:`A_{AC} = 0` and :math:`B_{AC} = 2`)
-are assigned into the ``None`` group and, thus, omitted from the analysis.
-
-
-Example
-=======
-
-Let's create a predicate to categorize the individuals
-to those having one missense allele or to those having
-one frameshift allele with respect to fictional transcript ``NM_1234.5``.
-
-We start by creating the variant predicates for missense (`A`)
-and frameshift (`B`) variants:
-
->>> from gpsea.model import VariantEffect
->>> from gpsea.analysis.predicate.genotype import VariantPredicates
->>> tx_id = "NM_1234.5"
->>> is_missense = VariantPredicates.variant_effect(effect=VariantEffect.MISSENSE_VARIANT, tx_id=tx_id)
->>> is_frameshift = VariantPredicates.variant_effect(effect=VariantEffect.FRAMESHIFT_VARIANT, tx_id=tx_id)
-
-Monoallelic predicate lets us customize the category names.
-Let's use `Missense` and `Frameshift` instead of the defaults `A` and `B`:
-
->>> a_label = "Missense"
->>> b_label = "Frameshift"
-
-Now we have all we need to create the monoallelic predicate:
-
->>> from gpsea.analysis.predicate.genotype import monoallelic_predicate
->>> gt_predicate = monoallelic_predicate(
-...     a_predicate=is_missense,
-...     b_predicate=is_frameshift,
-...     a_label=a_label, b_label=b_label,
-... )
->>> gt_predicate.display_question()
-'Allele group: Missense, Frameshift'
-
-`gt_predicate` can be used in any GPSEA analysis, to compare the individuals who harbor
-one allele of a missense variant with the individuals with one allele that leads to a frameshift.
-
-
-.. _biallelic-predicate:
-
-*******************
-Biallelic predicate
-*******************
-
-Biallelic predicate compares the individuals with *two* alleles of the variants of interest.
-The functionality is very similar to that of monoallelic predicate, with two differences.
-
-
-Categories
-==========
-
-A biallelic locus can be present in one of three genotypes.
-For example, using the predicates `A` and `B` for the missense vs. frameshift comparison as in the monoallelic case,
-we will assign the individuals into one of the following genotype categories:
-
-.. table:: Biallelic predicate genotype categories
-
-    +-------------------+-------------------+-----------+----------------+
-    | :math:`A_{AC}`    | :math:`B_{AC}`    | Category  | Category index |
-    +===================+===================+===========+================+
-    | 2                 | 0                 | A/A       | 0              |
-    +-------------------+-------------------+-----------+----------------+
-    | 1                 | 1                 | A/B       | 1              |
-    +-------------------+-------------------+-----------+----------------+
-    | 0                 | 2                 | B/B       | 2              |
-    +-------------------+-------------------+-----------+----------------+
-    | other             | other             | ``None``  |                |
-    +-------------------+-------------------+-----------+----------------+
-
-The individuals with a different allele count combination
-(e.g. :math:`A_{AC} = 1` and :math:`B_{AC} = 2`)
-are assigned into the ``None`` group and will be, thus, omitted from the analysis.
-
-    
-Partitions
-==========
-
-Sometimes we are interested in lumping several genotype categories into a group and then comparing the groups.
-For instance, we may want to compare phenotype of the individuals with *at least one* frameshift allele
-with those with *no* frameshift allele.
-
-The `partitions` option dictates form of the genotype categories.
-The option needs a set of sets of category indices (see table above).
-The set is a partition of a set with the following properties:
-
-* no subset is empty
-* the union of the subsets include all group indices
-* the intersection of any subsets is empty
-
-as in `partitions of a set <https://en.wikipedia.org/wiki/Partition_of_a_set>`_.
-
-
-Examples
-========
-
-Let `A` and `B` correspond to the variant predicates that select *MISSENSE* and *FRAMESHIFT* variants,
-and let's reuse the variant predicates ``is_missense`` and ``is_frameshift`` from the previous section.
-
-
-Compare missense vs. frameshift
--------------------------------
-
-We compare missense and frameshift variants in the context of an autosomal recessive disease,
-and we will use the same allele group names as before - `Missense` and `Frameshift`.
-
-
->>> from gpsea.analysis.predicate.genotype import biallelic_predicate
->>> gt_predicate = biallelic_predicate(
-...     a_predicate=is_missense,
-...     b_predicate=is_frameshift,
-...     a_label=a_label, b_label=b_label,
-... )
->>> gt_predicate.display_question()
-'Allele group: Missense/Missense, Missense/Frameshift, Frameshift/Frameshift'
-
-The predicate will assign the individuals into one of three genotype groups:
-
-* `Missense/Missense` - individual with two missense alleles
-* `Missense/Frameshift` - individual with one missense allele and one frameshift allele
-* `Frameshift/Frameshift` - individual with two frameshift alleles
-
-.. note::
-
-    This corresponds to the partitions::
-
-        partitions = ({0,}, {1,}, {2,})
-
-
-Compare missense vs. at least one frameshift allele
----------------------------------------------------
-
-Here we compare individuals with at least one frameshift allele with those with only missense variants.
-We can achieve this by providing the `partitions` option:
-
->>> partitions = ({0,}, {1, 2})
-
-The partitions includes subsets of the category indices to lump together.
-With reference to the *Biallelic predicate genotype categories* table,
-the first subset represents the individuals with `Missense/Missense` genotype
-and the second subset represents those with either `Missense/Frameshift` or `Frameshift/Frameshift`.
-
-We provide `partitions` as an extra argument
-to the :func:`~gpsea.analysis.predicate.genotype.biallelic_predicate` function:
-
->>> gt_predicate = biallelic_predicate(
-...     a_predicate=is_missense,
-...     b_predicate=is_frameshift,
-...     a_label=a_label, b_label=b_label,
-...     partitions=partitions,
-... )
->>> gt_predicate.display_question()
-'Allele group: Missense/Missense, Missense/Frameshift OR Frameshift/Frameshift'
-
diff --git a/docs/user-guide/predicates/diagnosis.rst b/docs/user-guide/predicates/diagnosis.rst
new file mode 100644
index 00000000..477a187c
--- /dev/null
+++ b/docs/user-guide/predicates/diagnosis.rst
@@ -0,0 +1,24 @@
+.. _group-by-diagnosis:
+
+==================
+Group by diagnosis
+==================
+
+We can easily compare individuals diagnosed with different diseases.
+:func:`~gpsea.analysis.predicate.genotype.diagnosis_predicate` groups the individuals
+based on the disease diagnoses present in the individuals.
+
+>>> from gpsea.analysis.predicate.genotype import diagnosis_predicate
+>>> gt_predicate = diagnosis_predicate(
+...     diagnoses=('OMIM:154700', 'OMIM:129600'),
+...     labels=('Marfan syndrome', 'Ectopia lentis, familial'),
+... )
+>>> gt_predicate.display_question()
+'What disease was diagnosed: OMIM:154700, OMIM:129600'
+
+The predicate takes two or more disease identifiers (`diagnoses`) as well as their names (`labels`),
+and it assigns the individuals based on their diagnoses.
+
+Note, the assignment must be unambiguous; any individual labeled with two or more target diagnoses
+(e.g. an individual diagnosed with both *Marfan syndrome* and *Ectopia lentis, familial* in the example above)
+will be *omitted* from the analysis.
diff --git a/docs/user-guide/predicates/diagnosis_predicate.rst b/docs/user-guide/predicates/diagnosis_predicate.rst
deleted file mode 100644
index 386af20a..00000000
--- a/docs/user-guide/predicates/diagnosis_predicate.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-.. _diagnosis-predicate:
-
-========================
-Partition by a diagnosis
-========================
-
-It is also possible to bin the individuals based on a diagnosis.
-The :func:`~gpsea.analysis.predicate.genotype.diagnosis_predicate` 
-prepares a genotype predicate for assigning an individual into a diagnosis group:
-
->>> from gpsea.analysis.predicate.genotype import diagnosis_predicate
->>> gt_predicate = diagnosis_predicate(
-...     diagnoses=('OMIM:154700', 'OMIM:129600'),
-...     labels=('Marfan syndrome', 'Ectopia lentis, familial'),
-... )
->>> gt_predicate.display_question()
-'What disease was diagnosed: OMIM:154700, OMIM:129600'
-
-Note, an individual must match only one diagnosis group. Any individuals labeled with two or more diagnoses
-(e.g. an individual with both *Marfan syndrome* and *Ectopia lentis, familial*)
-will be automatically omitted from the analysis.
\ No newline at end of file
diff --git a/docs/user-guide/predicates/genotype_predicates.rst b/docs/user-guide/predicates/genotype_predicates.rst
index 1ba42bb8..30c17aac 100644
--- a/docs/user-guide/predicates/genotype_predicates.rst
+++ b/docs/user-guide/predicates/genotype_predicates.rst
@@ -23,11 +23,8 @@ the :class:`~gpsea.analysis.predicate.genotype.VariantPredicate` is all about.
   :caption: Contents:
 
   variant_predicates
-  allele_count_predicates
-  mode_of_inheritance_predicate
-  male_female_predicate
-  diagnosis_predicate
-  groups_predicate
-
-
+  variant_category
+  allele_count
+  sex
+  diagnosis
 
diff --git a/docs/user-guide/predicates/groups_predicate.rst b/docs/user-guide/predicates/groups_predicate.rst
deleted file mode 100644
index a6142897..00000000
--- a/docs/user-guide/predicates/groups_predicate.rst
+++ /dev/null
@@ -1,41 +0,0 @@
-.. _groups-predicate:
-
-================
-Groups Predicate
-================
-
-
-
-Sometimes, all we want is to compare if there is a difference between individuals
-who include one or more alleles of variant `X` vs. individuals with variants `Y`,
-vs. individuals with variants `Z`, where `X`, `Y` and `Z` are variant predicates.
-We can do this with a *groups* predicate.
-
-The :func:`~gpsea.analysis.predicate.genotype.groups_predicate`
-takes *n* variant predicates and *n* group labels, and it will assign the patients
-into the respective groups if one or more matching allele is found.
-However, only one predicate is allowed to return a non-zero allele count.
-Otherwise, the patient is assigned with ``None`` and excluded from the analysis.
-
-Example
--------
-
-Here we show how to build a :class:`~gpsea.analysis.predicate.genotype.GenotypePolyPredicate`
-for testing if the individual has at least one missense vs. frameshift vs. synonymous variant.
-
->>> from gpsea.model import VariantEffect
->>> from gpsea.analysis.predicate.genotype import VariantPredicates, groups_predicate
->>> tx_id = 'NM_1234.5'
->>> gt_predicate = groups_predicate(
-...     predicates=(
-...         VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, tx_id),
-...         VariantPredicates.variant_effect(VariantEffect.FRAMESHIFT_VARIANT, tx_id),
-...         VariantPredicates.variant_effect(VariantEffect.SYNONYMOUS_VARIANT, tx_id),
-...     ),
-...     group_names=('Missense', 'Frameshift', 'Synonymous'),
-... )
->>> gt_predicate.display_question()
-'Genotype group: Missense, Frameshift, Synonymous'
-
-
-
diff --git a/docs/user-guide/predicates/male_female_predicate.rst b/docs/user-guide/predicates/male_female_predicate.rst
deleted file mode 100644
index 3bed237e..00000000
--- a/docs/user-guide/predicates/male_female_predicate.rst
+++ /dev/null
@@ -1,21 +0,0 @@
-.. _male-female-predicate:
-
-Partition by the sex of the individual
-======================================
-
-It is easy to investigate the phenotypic differences between females and males.
-The :func:`~gpsea.analysis.predicate.genotype.sex_predicate` provides a predicate
-for partitioning based on the sex of the individual:
-
->>> from gpsea.analysis.predicate.genotype import sex_predicate
->>> gt_predicate = sex_predicate()
->>> gt_predicate.display_question()
-'Sex of the individual: FEMALE, MALE'
-
-The individuals with :class:`~gpsea.model.Sex.UNKNOWN_SEX` will be omitted from the analysis.
-
-Note that we have implemented this predicate as a genotype predicate, because it is used in 
-place of other genotype predicates. Currently, it is not possible to compare the distribution of genotypes across sexes.
-
-
-
diff --git a/docs/user-guide/predicates/mode_of_inheritance_predicate.rst b/docs/user-guide/predicates/mode_of_inheritance_predicate.rst
deleted file mode 100644
index 49a3c487..00000000
--- a/docs/user-guide/predicates/mode_of_inheritance_predicate.rst
+++ /dev/null
@@ -1,175 +0,0 @@
-.. _mode-of-inheritance-predicate:
-
-==============================
-Mode of Inheritance Predicates
-==============================
-
-There are five basic modes of inheritance for single-gene diseases: autosomal dominant, 
-autosomal recessive, X-linked dominant, X-linked recessive, and mitochondrial 
-(See `Understanding Genetics, Appendix B <https://www.ncbi.nlm.nih.gov/books/NBK132145/>`_).
-
-GPSEA provides Mode of Inheritance predicates that categorize individuals
-based on the number of variant alleles. Clinical judgment should be used to choose the MoI
-for the cohort analysis.
-
-By default, MoI predicates use *all* variants recorded in the individual.
-However, a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate`
-can be provided to select the variants of interest. For instance, missense variants
-or the variants that overlap with a specific protein domain.
-
-
-.. _autosomal-dominant-predicate:
-
-****************************
-Autosomal dominant predicate
-****************************
-
-Autosomal dominant predicate counts the alleles of variants of interest
-to assign an individual into one of the following categories:
-
-.. table:: Autosomal dominant predicate categories
-
-    +------------------+----------------+
-    |   Allele count   |  Category      |
-    +==================+================+
-    |   0              |  `No allele`   |
-    +------------------+----------------+
-    |   1              |  `Monoallelic` |
-    +------------------+----------------+
-    |   :math:`\ge 2`  |  ``None``      |
-    +------------------+----------------+
-
-Examples
-========
-
-Use all variants
-----------------
-
-We can create the predicate with the :func:`~gpsea.analysis.predicate.genotype.autosomal_dominant` function:
-
->>> from gpsea.analysis.predicate.genotype import autosomal_dominant
->>> gt_predicate = autosomal_dominant()
->>> gt_predicate.display_question()
-'What is the genotype group: No allele, Monoallelic'
-
-
-Use a variant subset
---------------------
-
-To only consider a subset of variants, a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate` can be provided,
-to, for example, only include the missense variants on transcript ``NM_1234.5``:
-
->>> from gpsea.model import VariantEffect
->>> from gpsea.analysis.predicate.genotype import VariantPredicates
->>> is_missense = VariantPredicates.variant_effect(effect=VariantEffect.MISSENSE_VARIANT, tx_id="NM_1234.5")
->>> is_missense.get_question()
-'MISSENSE_VARIANT on NM_1234.5'
-
-
-.. _autosomal-recessive-predicate:
-
-*****************************
-Autosomal recessive predicate
-*****************************
-
-The autosomal recessive predicate uses the allele count to assign
-an individual into one of the genotype categories:
-
-.. table:: Autosomal recessive predicate categories
-
-    +------------------+-------------------+----------------+
-    |   Allele count   |  Category         | Category index |
-    +==================+===================+================+
-    |   0              |  `No allele`      | 0              |
-    +------------------+-------------------+----------------+
-    |   1              |  `Monoallelic`    | 1              |
-    +------------------+-------------------+----------------+
-    |   2              |  `Biallelic`      | 2              |
-    +------------------+-------------------+----------------+
-    |   :math:`\ge 3`  |  ``None``         |                |
-    +------------------+-------------------+----------------+
-
-.. note::
-
-    `Biallelic` includes both homozygous and compound heterozygous genotypes.
-
-
-Partitions
-==========
-
-Sometimes we are interested in lumping several genotype categories into a group or and then comparing the groups.
-For instance, to compare phenotype of the individuals with *at least one* frameshift allele
-with those with *no* frameshift allele. Alternatively, we may only want to analyze a subset of the genotype categories,
-such as `Monoallelic` vs. `Biallelic`.
-
-The `partitions` option of the :func:`~gpsea.analysis.predicate.genotype.autosomal_recessive` function
-lets us do this.
-The option needs a set of sets of category indices (see table above).
-The set is a partition of a set with the following properties:
-
-* no subset is empty
-* the intersection of any subsets is empty
-
-These rules are very much alike the properties of the `partitions of a set <https://en.wikipedia.org/wiki/Partition_of_a_set>`_,
-with the exception that adherence to *the union of the subsets include all group indices* rule is *NOT* required.
-
-
-Examples
-========
-
-
-Use all variants
-----------------
-
-We create the predicate
-with the :func:`~gpsea.analysis.predicate.genotype.autosomal_recessive` function:
-
->>> from gpsea.analysis.predicate.genotype import autosomal_recessive
->>> gt_predicate = autosomal_recessive()
->>> gt_predicate.display_question()
-'What is the genotype group: No allele, Monoallelic, Biallelic'
-
-
-Use a variant subset
---------------------
-
-Same as in the autosomal dominant version,
-we can use a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate`
-to only count the alleles of the variants of interest, such as the missense variants
-of a fictional transcript ``NM_1234.5``:
-
->>> from gpsea.model import VariantEffect
->>> from gpsea.analysis.predicate.genotype import VariantPredicates
->>> is_missense = VariantPredicates.variant_effect(effect=VariantEffect.MISSENSE_VARIANT, tx_id="NM_1234.5")
->>> is_missense.get_question()
-'MISSENSE_VARIANT on NM_1234.5'
-
-and then use it to create the autosomal recessive predicate:
-
->>> gt_predicate = autosomal_recessive(is_missense)
->>> gt_predicate.display_question()
-'What is the genotype group: No allele, Monoallelic, Biallelic'
-
-This predicate will assign the individuals into one of the listed genotype categories
-based on the allele counts of the missense variants.
-
-
-Compare `Monoallelic` vs. `Biallelic`
--------------------------------------
-
-We can provide ``partitions`` to only compare the heterozygotes with those carrying
-biallelic alt mutations (homozygous alternate or compound heterozygous):
-
-We consult the *Autosomal recessive predicate categories* table for the category indices
-and we create the genotype group partitions:
-
->>> # `1` for `Monoallelic` and `2` for `Biallelic`
->>> partitions = ({1,}, {2,})
-
-which we use to create the autosomal recessive predicate:
-
->>> gt_predicate = autosomal_recessive(
-...     partitions=partitions,    
-... )
->>> gt_predicate.display_question()
-'What is the genotype group: Monoallelic, Biallelic'
diff --git a/docs/user-guide/predicates/sex.rst b/docs/user-guide/predicates/sex.rst
new file mode 100644
index 00000000..725f6362
--- /dev/null
+++ b/docs/user-guide/predicates/sex.rst
@@ -0,0 +1,20 @@
+.. _group-by-sex:
+
+============
+Group by sex
+============
+
+It is easy to investigate the differences between males and females.
+The :func:`~gpsea.analysis.predicate.genotype.sex_predicate` partitions
+the individuals based on their :class:`~gpsea.model.Sex`:
+
+>>> from gpsea.analysis.predicate.genotype import sex_predicate
+>>> gt_predicate = sex_predicate()
+>>> gt_predicate.display_question()
+'Sex of the individual: FEMALE, MALE'
+
+The individuals with :class:`~gpsea.model.Sex.UNKNOWN_SEX` will be omitted from the analysis.
+
+Note that we implemented this predicate as a genotype predicate.
+Currently, it is not possible to compare the distribution of genotypes across sexes.
+
diff --git a/docs/user-guide/predicates/variant_category.rst b/docs/user-guide/predicates/variant_category.rst
new file mode 100644
index 00000000..e9816239
--- /dev/null
+++ b/docs/user-guide/predicates/variant_category.rst
@@ -0,0 +1,198 @@
+.. _variant-category:
+
+=========================
+Group by variant category
+=========================
+
+Sometimes we want to compare the individuals who have the same allele count (`AC`) of variant categories :math:`A` and :math:`B`.
+For example, in the context of an autosomal dominant disease,
+we may want to compare the individuals with :math:`AC_{A}=1` (where :math:`A` is e.g. a predicted loss-of-function mutation)
+with those harboring :math:`AC_{B}=1` (where :math:`B` is e.g. a missense mutation).
+Similarly, in an autosomal recessive disease, we may be interested in comparing the individuals
+with :math:`AC_{A} \ge 1` with those with :math:`AC_{A} = 0`.
+In both analyses, we compare two variant categories :math:`A` and :math:`B`
+which are described by a :class:`~gpsea.analysis.predicate.genotype.VariantPredicate`
+(see :ref:`variant_predicates` section),
+while ensuring the allele count sum of both variant categories is :math:`k`.
+
+:math:`k = \sum_{i \in \{A, B\}} AC_{i}`
+
+GPSEA provides two predicates:
+
+
+.. table::
+
+    +-----------+-----------------------+------------------------------------------------------------------+
+    | :math:`k` | Name                  | Function                                                         |
+    +===========+=======================+==================================================================+
+    | 1         | Monoallelic predicate | :func:`~gpsea.analysis.predicate.genotype.monoallelic_predicate` |
+    +-----------+-----------------------+------------------------------------------------------------------+
+    | 2         | Biallelic predicate   | :func:`~gpsea.analysis.predicate.genotype.biallelic_predicate`   |
+    +-----------+-----------------------+------------------------------------------------------------------+
+
+
+.. _monoallelic-predicate:
+
+*********************
+Monoallelic predicate
+*********************
+
+Monoallelic predicate compares individuals who have *one* allele of a variants of interest.
+The predicate needs two variant predicates `A` and `B`
+to compute the allele counts :math:`AC_{A}` and :math:`AC_{B}`,
+in order to assign an individual into one of the following genotype groups:
+
+.. table:: Monoallelic predicate genotype groups
+
+    +-----------------+-------------------+-------------------+
+    | Genotype group  | :math:`AC_{A}`    | :math:`AC_{B}`    |
+    +=================+===================+===================+
+    | A               | 1                 | 0                 |
+    +-----------------+-------------------+-------------------+
+    | B               | 0                 | 1                 |
+    +-----------------+-------------------+-------------------+
+    | ``None``        | other             | other             |
+    +-----------------+-------------------+-------------------+
+
+The individuals with :math:`\sum_{i \in \{A, B\}} AC_{i} \neq 1`
+are omitted from the analysis.
+
+
+Example
+=======
+
+Let's create a predicate to categorize the individuals
+to those having one missense allele or to those having
+one frameshift allele with respect to fictional transcript ``NM_1234.5``.
+
+>>> tx_id = "NM_1234.5"
+
+We start by creating the variant predicates for missense (`A`)
+and frameshift (`B`) variants:
+
+>>> from gpsea.model import VariantEffect
+>>> from gpsea.analysis.predicate.genotype import VariantPredicates
+>>> is_missense = VariantPredicates.variant_effect(effect=VariantEffect.MISSENSE_VARIANT, tx_id=tx_id)
+>>> is_frameshift = VariantPredicates.variant_effect(effect=VariantEffect.FRAMESHIFT_VARIANT, tx_id=tx_id)
+
+Monoallelic predicate lets us customize the category names.
+Let's use `Missense` and `Frameshift` instead of the defaults:
+
+>>> a_label = "Missense"
+>>> b_label = "Frameshift"
+
+Now we have all we need to create the predicate:
+
+>>> from gpsea.analysis.predicate.genotype import monoallelic_predicate
+>>> gt_predicate = monoallelic_predicate(
+...     a_predicate=is_missense,
+...     b_predicate=is_frameshift,
+...     a_label=a_label, b_label=b_label,
+... )
+>>> gt_predicate.display_question()
+'Allele group: Missense, Frameshift'
+
+
+
+.. _biallelic-predicate:
+
+*******************
+Biallelic predicate
+*******************
+
+Biallelic predicate compares the individuals with *two* alleles of the variants of interest.
+The functionality is very similar to that of monoallelic predicate, with two differences.
+
+
+Categories
+==========
+
+Biallelic locus can be present in one of three genotypes, allowing an individual
+to be assigned into one of the three genotype groups:
+
+.. _biallelic-predicate-gt-groups:
+
+.. table:: Biallelic predicate genotype groups
+
+    +-------+----------------+-------------------+-------------------+
+    | Index | Genotype group | :math:`AC_{A}`    | :math:`AC_{B}`    |
+    +=======+================+===================+===================+
+    | 0     | A/A            | 2                 | 0                 |
+    +-------+----------------+-------------------+-------------------+
+    | 1     | A/B            | 1                 | 1                 |
+    +-------+----------------+-------------------+-------------------+
+    | 2     | B/B            | 0                 | 2                 |
+    +-------+----------------+-------------------+-------------------+
+    |       | ``None``       | other             | other             |
+    +-------+----------------+-------------------+-------------------+
+
+Note that :math:`\sum_{i \in \{A, B\}} AC_{i} = 2` and the individuals
+with a different allele count sum are omitted from the analysis.
+
+Example
+-------
+
+Let `A` and `B` correspond to *MISSENSE* and *FRAMESHIFT* variants,
+and let's reuse the variant predicates ``is_missense`` and ``is_frameshift`` from the previous section,
+to compare missense and frameshift variants in the context of an autosomal recessive disease.
+
+>>> from gpsea.analysis.predicate.genotype import biallelic_predicate
+>>> gt_predicate = biallelic_predicate(
+...     a_predicate=is_missense,
+...     b_predicate=is_frameshift,
+...     a_label="Missense", b_label="Frameshift",
+... )
+>>> gt_predicate.display_question()
+'Allele group: Missense/Missense, Missense/Frameshift, Frameshift/Frameshift'
+
+The predicate will assign the individuals into one of three genotype groups:
+
+* `Missense/Missense` - two missense alleles
+* `Missense/Frameshift` - one missense allele and one frameshift allele
+* `Frameshift/Frameshift` - two frameshift alleles
+
+    
+Partitions
+==========
+
+Sometimes we are interested in lumping several genotype groups into a partition
+and then comparing the partitions.
+For instance, in the context of an autosomal recessive disease,
+we may want to compare individuals with two "mild" mutations with the individuals
+with at least one "severe" mutation.
+This comparison can be implemented using the `partitions` option.
+
+We define a partition as a set of one or more genotype group indices
+(see :ref:`biallelic-predicate-gt-groups` table),
+and we must provide at least two such partitions to the `partitions` option.
+
+
+Example
+-------
+
+Let `A` and `B` correspond to *MISSENSE* and *FRAMESHIFT* variant
+Here we compare the individuals with two missense alleles with the individuals with one frameshift and one missense alleles,
+or with two frameshift alelles.
+
+The partition for the two missense alleles will include the genotype group `0`,
+and the one or more frameshift alleles partition corresponds to the genotype groups `{1, 2}` (see :ref:`biallelic-predicate-gt-groups` table).
+The complete partitions are defined as:
+
+>>> partitions = ({0,}, {1, 2})
+
+We provide `partitions`
+to the :func:`~gpsea.analysis.predicate.genotype.biallelic_predicate` function:
+
+>>> gt_predicate = biallelic_predicate(
+...     a_predicate=is_missense,
+...     b_predicate=is_frameshift,
+...     a_label="Missense", b_label="Frameshift",
+...     partitions=partitions,
+... )
+>>> gt_predicate.display_question()
+'Allele group: Missense/Missense, Missense/Frameshift OR Frameshift/Frameshift'
+
+Now `gt_predicate` assigns an individual into one of the two categories:
+
+* `Missense/Missense`
+* `Missense/Frameshift` OR `Frameshift/Frameshift`
diff --git a/src/gpsea/analysis/mtc_filter/_impl.py b/src/gpsea/analysis/mtc_filter/_impl.py
index f9415288..02609a21 100644
--- a/src/gpsea/analysis/mtc_filter/_impl.py
+++ b/src/gpsea/analysis/mtc_filter/_impl.py
@@ -493,12 +493,15 @@ def filter(
     def possible_results(self) -> typing.Collection[PhenotypeMtcResult]:
         return (
             PhenotypeMtcFilter.OK,
-            HpoMtcFilter.SKIPPING_GENERAL_TERM,
-            HpoMtcFilter.SKIPPING_NON_PHENOTYPE_TERM,
-            self._below_frequency_threshold,
-            self._not_powered_for_2_by_2,
-            HpoMtcFilter.NO_GENOTYPE_HAS_MORE_THAN_ONE_HPO,
-            HpoMtcFilter.SKIPPING_SINCE_ONE_GENOTYPE_HAD_ZERO_OBSERVATIONS,
+            self._below_frequency_threshold,  # HMF01
+            HpoMtcFilter.NO_GENOTYPE_HAS_MORE_THAN_ONE_HPO,  # HMF02
+            HpoMtcFilter.SAME_COUNT_AS_THE_ONLY_CHILD,  # HMF03
+            HpoMtcFilter.SKIPPING_SINCE_ONE_GENOTYPE_HAD_ZERO_OBSERVATIONS,  # HMF05
+            self._not_powered_for_2_by_2,  # HMF06
+            self._not_powered_for_2_by_3,  # HMF06
+            HpoMtcFilter.SKIPPING_NON_PHENOTYPE_TERM,  # HMF07
+            HpoMtcFilter.SKIPPING_GENERAL_TERM,  # HMF08
+            self._below_annotation_frequency_threshold,  # HMF09
         )
 
     def filter_method_name(self) -> str:
diff --git a/src/gpsea/analysis/predicate/genotype/__init__.py b/src/gpsea/analysis/predicate/genotype/__init__.py
index 59be80fe..71b52776 100644
--- a/src/gpsea/analysis/predicate/genotype/__init__.py
+++ b/src/gpsea/analysis/predicate/genotype/__init__.py
@@ -1,16 +1,16 @@
 from ._api import GenotypePolyPredicate
 from ._api import VariantPredicate
 from ._counter import AlleleCounter
-from ._gt_predicates import groups_predicate, sex_predicate, diagnosis_predicate
-from ._gt_predicates import autosomal_dominant, autosomal_recessive
+from ._gt_predicates import sex_predicate, diagnosis_predicate
 from ._gt_predicates import monoallelic_predicate, biallelic_predicate
+from ._gt_predicates import allele_count
 from ._variant import VariantPredicates, ProteinPredicates
 
 __all__ = [
     'GenotypePolyPredicate',
-    'groups_predicate', 'sex_predicate', 'diagnosis_predicate',
-    'autosomal_dominant', 'autosomal_recessive',
+    'sex_predicate', 'diagnosis_predicate',
     'monoallelic_predicate', 'biallelic_predicate',
+    'allele_count',
     'AlleleCounter', 'VariantPredicate',
     'VariantPredicates', 'ProteinPredicates',
 ]
diff --git a/src/gpsea/analysis/predicate/genotype/_gt_predicates.py b/src/gpsea/analysis/predicate/genotype/_gt_predicates.py
index 61323b27..eee5bcf8 100644
--- a/src/gpsea/analysis/predicate/genotype/_gt_predicates.py
+++ b/src/gpsea/analysis/predicate/genotype/_gt_predicates.py
@@ -1,11 +1,10 @@
-import dataclasses
 import typing
 
-from collections import defaultdict, Counter
+from collections import Counter
 
 import hpotk
 
-from gpsea.model import Patient, Sex
+from gpsea.model import Patient
 
 from .._api import Categorization, PatientCategory
 from ._api import GenotypePolyPredicate
@@ -14,109 +13,7 @@
 from ._variant import VariantPredicates
 
 
-class AlleleCountingGroupsPredicate(GenotypePolyPredicate):
-    # NOT PART OF THE PUBLIC API
-
-    def __init__(
-        self,
-        counters: typing.Iterable[AlleleCounter],
-        categorizations: typing.Iterable[Categorization],
-    ):
-        self._counters = tuple(counters)
-        self._categorizations = tuple(categorizations)
-        self._question = "Genotype group"
-
-    def get_categorizations(self) -> typing.Sequence[Categorization]:
-        return self._categorizations
-
-    def get_question_base(self) -> str:
-        return self._question
-
-    def test(self, patient: Patient) -> typing.Optional[Categorization]:
-        self._check_patient(patient)
-
-        nonzero_counter_idxs = []
-        for i, counter in enumerate(self._counters):
-            allele_count = counter.count(patient)
-            if allele_count > 0:
-                nonzero_counter_idxs.append(i)
-
-        if len(nonzero_counter_idxs) == 1:
-            return self._categorizations[nonzero_counter_idxs[0]]
-        else:
-            # Patient can be assigned either into no group or into multiple groups.
-            return None
-
-    def __eq__(self, value: object) -> bool:
-        return (
-            isinstance(value, AlleleCountingGroupsPredicate)
-            and self._counters == value._counters
-            and self._categorizations == value._categorizations
-        )
-
-    def __hash__(self) -> int:
-        return hash(
-            (
-                self._counters,
-                self._categorizations,
-            )
-        )
-
-    def __str__(self) -> str:
-        return self.get_question_base()
-
-    def __repr__(self) -> str:
-        return (
-            "AlleleCountingGroupsPredicate("
-            + "counters={self._counters}, "
-            + "categorizations={self._categorizations})"
-        )
-
-
-def groups_predicate(
-    predicates: typing.Iterable[VariantPredicate],
-    group_names: typing.Iterable[str],
-) -> GenotypePolyPredicate:
-    """
-    Create a genotype predicate that bins the patient into one of *n* groups.
-
-    The genotype groups *should* not overlap.
-    In case of an overlap, the patient will be assigned into no group (`None`).
-
-    See the :ref:`groups-predicate` section for an example.
-
-    :param predicates: an iterable with at least 2 variant predicates to determine a genotype group.
-    :param group_names: an iterable with group names. The number of group names must match the number of predicates.
-    """
-    # First, collect the iterables and check sanity.
-    predicates = tuple(predicates)
-    group_names = tuple(group_names)
-
-    assert len(predicates) >= 2, f"We need at least 2 predicates: {len(predicates)}"
-    assert len(predicates) == len(
-        group_names
-    ), f"The number of group names must match the number of predicates: {len(group_names)}!={len(predicates)}"
-
-    # Then, prepare the counters and categorizations.
-    counters = [AlleleCounter(predicate=predicate) for predicate in predicates]
-
-    categorizations = []
-    for i, (predicate, name) in enumerate(zip(predicates, group_names)):
-        categorization = Categorization.from_raw_parts(
-            cat_id=i,
-            name=name,
-            description=predicate.get_question(),
-        )
-        categorizations.append(categorization)
-
-    # Last, put the predicate together.
-    return AlleleCountingGroupsPredicate(
-        counters=counters,
-        categorizations=categorizations,
-    )
-
-
-def fixate_partitions(
+def _fixate_partitions(
     partitions: typing.Collection[typing.Collection[int]],
 ) -> typing.Collection[typing.Sequence[int]]:
     fixed = []
@@ -125,7 +22,7 @@ def fixate_partitions(
     return fixed
 
 
-def qc_partitions(
+def _qc_partitions(
     partitions: typing.Collection[typing.Collection[int]],
 ):
     # NOT PART OF THE PUBLIC API
@@ -164,7 +61,7 @@ def qc_partitions(
         raise ValueError(", ".join(errors))
 
 
-def build_count_to_cat(
+def _build_count_to_cat(
     a_label: str,
     b_label: str,
     partitions: typing.Iterable[typing.Iterable[int]],
@@ -196,7 +93,7 @@ def build_count_to_cat(
     return ac2cat
 
 
-def deduplicate_categorizations(
+def _deduplicate_categorizations(
     cats: typing.Iterable[Categorization],
 ) -> typing.Sequence[Categorization]:
     return sorted(
@@ -205,6 +102,22 @@ def deduplicate_categorizations(
     )
 
 
+def _compute_hash(
+    count2cat: typing.Mapping[typing.Any, typing.Any],
+    counters: typing.Iterable[typing.Hashable],
+) -> int:
+    hash_value = 17
+
+    for key, val in count2cat.items():
+        hash_value += 13 * hash(key)
+        hash_value += 13 * hash(val)
+
+    for counter in counters:
+        hash_value += 23 * hash(counter)
+
+    return hash_value
+
+
 class PolyCountingGenotypePredicate(GenotypePolyPredicate):
     # NOT PART OF THE PUBLIC API
 
@@ -242,7 +155,7 @@ def biallelic(
         b_label: str,
         partitions: typing.Iterable[typing.Iterable[int]],
     ) -> "PolyCountingGenotypePredicate":
-        count2cat = build_count_to_cat(
+        count2cat = _build_count_to_cat(
             a_label=a_label,
             b_label=b_label,
             partitions=partitions,
@@ -273,23 +186,10 @@ def __init__(
         b_counter: AlleleCounter,
     ):
         self._count2cat = dict(count2cat)
-        self._categorizations = tuple(deduplicate_categorizations(count2cat.values()))
+        self._categorizations = tuple(_deduplicate_categorizations(count2cat.values()))
         self._a_counter = a_counter
         self._b_counter = b_counter
-        self._hash = self._compute_hash()
-
-    def _compute_hash(self) -> int:
-        hash_value = 17
-
-        self._groups = defaultdict(list)
-        for count, cat in self._count2cat.items():
-            hash_value += 13 * hash(count)
-            hash_value += 13 * hash(cat)
-
-        hash_value += 23 * hash(self._a_counter)
-        hash_value += 23 * hash(self._b_counter)
-
-        return hash_value
+        self._hash = _compute_hash(self._count2cat, (self._a_counter, self._b_counter))
 
     def get_categorizations(self) -> typing.Sequence[Categorization]:
         return self._categorizations
@@ -320,13 +220,13 @@ def __hash__(self) -> int:
 
 def monoallelic_predicate(
     a_predicate: VariantPredicate,
-    b_predicate: VariantPredicate,
+    b_predicate: typing.Optional[VariantPredicate] = None,
     a_label: str = "A",
     b_label: str = "B",
 ) -> GenotypePolyPredicate:
     """
     The predicate bins patient into one of two groups, `A` and `B`,
-    based on presence of *exactly* one allele of a variant
+    based on presence of *exactly one* allele of a variant
     that meets the predicate criteria.
 
     See :ref:`monoallelic-predicate` for more information and an example usage.
@@ -334,13 +234,17 @@ def monoallelic_predicate(
     :param a_predicate: predicate to test if the variants
         meet the criteria of the first group (named `A` by default).
     :param b_predicate: predicate to test if the variants
-        meet the criteria of the second group (named `B` by default).
+        meet the criteria of the second group or `None`
+        if the inverse of `a_predicate` should be used (named `B` by default).
     :param a_label: display name of the `a_predicate` (default ``"A"``).
     :param b_label: display name of the `b_predicate` (default ``"B"``).
     """
     assert isinstance(a_label, str)
     assert isinstance(b_label, str)
 
+    if b_predicate is None:
+        b_predicate = ~a_predicate
+
     return PolyCountingGenotypePredicate.monoallelic(
         a_predicate=a_predicate,
         b_predicate=b_predicate,
@@ -351,7 +255,7 @@ def monoallelic_predicate(
 
 def biallelic_predicate(
     a_predicate: VariantPredicate,
-    b_predicate: VariantPredicate,
+    b_predicate: typing.Optional[VariantPredicate] = None,
     a_label: str = "A",
     b_label: str = "B",
     partitions: typing.Collection[typing.Collection[int]] = ((0,), (1,), (2,)),
@@ -364,8 +268,11 @@ def biallelic_predicate(
 
     See :ref:`biallelic-predicate` for more information and an example usage.
 
-    :param a_predicate: predicate to test if the variants meet the criteria of the first group (named `A` by default).
-    :param b_predicate: predicate to test if the variants meet the criteria of the second group (named `B` by default).
+    :param a_predicate: predicate to test if the variants meet
+        the criteria of the first group (named `A` by default).
+    :param b_predicate: predicate to test if the variants meet
+        the criteria of the second group or `None` if an inverse
+        of `a_predicate` should be used (named `B` by default).
     :param a_label: display name of the `a_predicate` (default ``"A"``).
     :param b_label: display name of the `b_predicate` (default ``"B"``).
     :param partitions: a sequence with partition identifiers (default ``((0,), (1,), (2,))``).
@@ -374,8 +281,11 @@ def biallelic_predicate(
     assert isinstance(a_label, str)
     assert isinstance(b_label, str)
 
-    partitions = fixate_partitions(partitions)
-    qc_partitions(partitions)
+    partitions = _fixate_partitions(partitions)
+    _qc_partitions(partitions)
+
+    if b_predicate is None:
+        b_predicate = ~a_predicate
 
     return PolyCountingGenotypePredicate.biallelic(
         a_predicate=a_predicate,
@@ -386,296 +296,116 @@ def biallelic_predicate(
     )
 
 
-def autosomal_dominant(
-    variant_predicate: typing.Optional[VariantPredicate] = None,
-) -> GenotypePolyPredicate:
-    """
-    Create a predicate that assigns the patient either
-    into homozygous reference or heterozygous
-    group in line with the autosomal dominant mode of inheritance.
-
-    See :ref:`mode-of-inheritance-predicate` for more info and an example usage.
-
-    :param variant_predicate: a predicate for choosing the variants for testing
-        or `None` if all variants should be used.
-    """
-    if variant_predicate is None:
-        variant_predicate = VariantPredicates.true()
-
-    return ModeOfInheritancePredicate.from_moi_info(
-        variant_predicate=variant_predicate,
-        mode_of_inheritance_data=ModeOfInheritanceInfo.autosomal_dominant(),
-        partitions=((0,), (1,)),
+def _build_ac_to_cat(
+    partitions: typing.Collection[typing.Collection[int]],
+) -> typing.Mapping[int, Categorization]:
+    labels = (
+        "No allele",
+        "One allele",
+        "Two alleles",
     )
 
+    ac2cat = {}
+    for i, partition in enumerate(partitions):
+        name = " OR ".join(str(j) for j in partition)
+        description = " OR ".join(labels[j] for j in partition)
+        cat = Categorization(
+            PatientCategory(cat_id=i, name=name, description=description),
+        )
+        for j in partition:
+            ac2cat[j] = cat
 
-def autosomal_recessive(
-    variant_predicate: typing.Optional[VariantPredicate] = None,
-    partitions: typing.Collection[typing.Collection[int]] = ((0,), (1,), (2,)),
-) -> GenotypePolyPredicate:
-    """
-    Create a predicate that assigns the patient either into
-    homozygous reference, heterozygous, or biallelic alternative allele
-    (homozygous alternative or compound heterozygous)
-    group in line with the autosomal recessive mode of inheritance.
+    return ac2cat
 
-    See :ref:`mode-of-inheritance-predicate` for more info and an example usage.
 
-    :param variant_predicate: a predicate for choosing the variants for testing
-        or `None` if all variants should be used
-    :param partitions: a sequence with partition identifiers (default ``((0,), (1,), (2,))``).
+def allele_count(
+    counts: typing.Collection[typing.Collection[int]],
+    target: typing.Optional[VariantPredicate] = None,
+) -> GenotypePolyPredicate:
     """
-    if variant_predicate is None:
-        variant_predicate = VariantPredicates.true()
-
-    partitions = fixate_partitions(partitions)
-
-    return ModeOfInheritancePredicate.from_moi_info(
-        variant_predicate=variant_predicate,
-        mode_of_inheritance_data=ModeOfInheritanceInfo.autosomal_recessive(),
-        partitions=partitions,
-    )
-
-
-@dataclasses.dataclass(frozen=True)
-class GenotypeGroup:
-    allele_count: int
-    sex: typing.Optional[Sex]
-    categorization: Categorization
-
-
-class ModeOfInheritanceInfo:
-
-    # NOT PART OF THE PUBLIC API!!!
-
-    HOM_REF = Categorization(
-        PatientCategory(
-            cat_id=0,
-            name="No allele",
-            description="No allele",
-        ),
-    )
-    HET = Categorization(
-        PatientCategory(
-            cat_id=1,
-            name="Monoallelic",
-            description="Monoallelic",
-        ),
-    )
-    BIALLELIC_ALT = Categorization(
-        PatientCategory(
-            cat_id=2,
-            name="Biallelic",
-            description="Homozygous alternate or compound heterozygous",
-        ),
-    )
-
-    @staticmethod
-    def autosomal_dominant() -> "ModeOfInheritanceInfo":
-        groups = (
-            GenotypeGroup(
-                allele_count=0,
-                sex=None,
-                categorization=ModeOfInheritanceInfo.HOM_REF,
-            ),
-            GenotypeGroup(
-                allele_count=1,
-                sex=None,
-                categorization=ModeOfInheritanceInfo.HET,
-            ),
-        )
-        return ModeOfInheritanceInfo(
-            groups=groups,
-        )
-
-    @staticmethod
-    def autosomal_recessive() -> "ModeOfInheritanceInfo":
-        groups = (
-            GenotypeGroup(
-                allele_count=0,
-                sex=None,
-                categorization=ModeOfInheritanceInfo.HOM_REF,
-            ),
-            GenotypeGroup(
-                allele_count=1,
-                sex=None,
-                categorization=ModeOfInheritanceInfo.HET,
-            ),
-            GenotypeGroup(
-                allele_count=2,
-                sex=None,
-                categorization=ModeOfInheritanceInfo.BIALLELIC_ALT,
-            ),
-        )
-        return ModeOfInheritanceInfo(
-            groups=groups,
-        )
-
-    def __init__(
-        self,
-        groups: typing.Iterable[GenotypeGroup],
-    ):
-        # We want this to be hashable but also keep a non-hashable dict
-        # as a field. Therefore, we pre-compute the hash manually.
-        # The correctness depends on two default dicts with same keys and values
-        # comparing equal.
-        hash_value = 17
-
-        self._groups = defaultdict(list)
-        for group in groups:
-            assert isinstance(group, GenotypeGroup)
-            self._groups[group.allele_count].append(group)
-            hash_value += 13 * hash(group)
-
-        self._hash = hash_value
-
-    @property
-    def groups(self) -> typing.Iterator[GenotypeGroup]:
-        # Flatten `values()` which is an iterable of lists.
-        return (group for meta_group in self._groups.values() for group in meta_group)
-
-    def get_groups_for_allele_count(
-        self,
-        allele_count: int,
-    ) -> typing.Sequence[GenotypeGroup]:
-        try:
-            return self._groups[allele_count]
-        except KeyError:
-            # No group for this allele count is OK
-            return ()
+    Create a predicate to assign the patient into a group based on the allele count
+    of the target variants.
 
-    def __eq__(self, value: object) -> bool:
-        return (
-            isinstance(value, ModeOfInheritanceInfo) and self._groups == value._groups
-        )
+    The `counts` option takes a collection of `int` collections.
+    Each inner collection corresponds to an allele count partition
+    and the outer collection includes all partitions.
+    The `int` represents the target allele count.
+    A count can be included only in one partition.
 
-    def __hash__(self) -> int:
-        return self._hash
+    Examples
+    --------
 
-    def __str__(self) -> str:
-        return f"ModeOfInheritanceInfo(groups={self._groups})"
+    The following counts will partition the cohort into individuals
+    with zero allele or one target allele:
 
-    def __repr__(self) -> str:
-        return str(self)
+    >>> from gpsea.analysis.predicate.genotype import allele_count
+    >>> zero_vs_one = allele_count(counts=({0,}, {1,}))
+    >>> zero_vs_one.display_question()
+    'Allele count: 0, 1'
+    
+    These counts will create three groups for individuals with zero, one or two alleles:
 
+    >>> zero_vs_one_vs_two = allele_count(counts=({0,}, {1,}, {2,}))
+    >>> zero_vs_one_vs_two.display_question()
+    'Allele count: 0, 1, 2'
 
-class ModeOfInheritancePredicate(GenotypePolyPredicate):
-    # NOT PART OF THE PUBLIC API!!!
-    """
-    `ModeOfInheritancePredicate` assigns an individual into a group based on compatibility with
-    the selected mode of inheritance.
+    :param counts: a sequence with allele count partitions.
+    :param target: a predicate for choosing the variants for testing
+        or `None` if *all* variants in the individual should be used.
     """
+    if target is None:
+        target = VariantPredicates.true()
+    else:
+        assert isinstance(target, VariantPredicate)
 
-    @staticmethod
-    def from_moi_info(
-        variant_predicate: VariantPredicate,
-        mode_of_inheritance_data: ModeOfInheritanceInfo,
-        partitions: typing.Collection[typing.Collection[int]],
-    ) -> "ModeOfInheritancePredicate":
-        """
-        Create a predicate for specified mode of inheritance data.
-        """
-        qc_partitions(partitions=partitions)
-
-        allele_counter = AlleleCounter(predicate=variant_predicate)
-        count2cat = ModeOfInheritancePredicate.prepare_count2cat(
-            mode_of_inheritance_data=mode_of_inheritance_data,
-            partitions=partitions,
-        )
-        return ModeOfInheritancePredicate(
-            allele_counter=allele_counter,
-            count2cat=count2cat,
-        )
+    counts = _fixate_partitions(counts)
+    _qc_partitions(counts)
 
-    @staticmethod
-    def prepare_count2cat(
-        mode_of_inheritance_data: ModeOfInheritanceInfo,
-        partitions: typing.Collection[typing.Collection[int]],
-    ) -> typing.Mapping[int, Categorization]:
-        groups = tuple(mode_of_inheritance_data.groups)
-        partition_to_allele_count = tuple(range(len(groups)))
-        partition_to_label = tuple(
-            group.categorization.category.name for group in groups
-        )
+    count2cat = _build_ac_to_cat(counts)
 
-        count2cat = {}
-        for i, partition in enumerate(partitions):
-            label = " OR ".join(partition_to_label[j] for j in partition)
+    counter = AlleleCounter(predicate=target)
+    
+    return AlleleCountPredicate(
+        count2cat=count2cat,
+        counter=counter,
+    )
 
-            cat = Categorization(
-                PatientCategory(
-                    cat_id=i,
-                    name=label,
-                    description=label,
-                )
-            )
-            for p in partition:
-                allele_count = partition_to_allele_count[p]
-                count2cat[allele_count] = cat
 
-        return count2cat
+class AlleleCountPredicate(GenotypePolyPredicate):
 
     def __init__(
         self,
-        allele_counter: AlleleCounter,
         count2cat: typing.Mapping[int, Categorization],
+        counter: AlleleCounter,
     ):
-        assert isinstance(allele_counter, AlleleCounter)
-        self._allele_counter = allele_counter
-
         self._count2cat = dict(count2cat)
-        self._categorizations = tuple(deduplicate_categorizations(count2cat.values()))
 
-        self._question = "What is the genotype group"
-        self._hash = self._compute_hash()
+        assert isinstance(counter, AlleleCounter)
+        self._counter = counter
 
-    def _compute_hash(self) -> int:
-        hash_value = 17
-
-        self._groups = defaultdict(list)
-        for count, cat in self._count2cat.items():
-            hash_value += 13 * hash(count)
-            hash_value += 13 * hash(cat)
-
-        hash_value += 23 * hash(self._allele_counter)
-
-        return hash_value
+        self._categorizations = tuple(_deduplicate_categorizations(self._count2cat.values()))
+        self._hash = _compute_hash(self._count2cat, (self._counter,))
 
     def get_categorizations(self) -> typing.Sequence[Categorization]:
         return self._categorizations
 
     def get_question_base(self) -> str:
-        return self._question
+        return "Allele count"
 
-    def test(
-        self,
-        patient: Patient,
-    ) -> typing.Optional[Categorization]:
+    def test(self, patient: Patient) -> typing.Optional[Categorization]:
         self._check_patient(patient)
 
-        allele_count = self._allele_counter.count(patient)
-        return self._count2cat.get(allele_count, None)
+        count = self._counter.count(patient)
+        return self._count2cat.get(count, None)
 
     def __eq__(self, value: object) -> bool:
-        return (
-            isinstance(value, ModeOfInheritancePredicate)
-            and self._allele_counter == value._allele_counter
-            and self._count2cat == value._count2cat
-        )
+        return isinstance(value, AlleleCountPredicate) \
+            and self._count2cat == value._count2cat \
+            and self._counter == value._counter
 
     def __hash__(self) -> int:
         return self._hash
 
-    def __str__(self) -> str:
-        return (
-            "ModeOfInheritancePredicate("
-            f"allele_counter={self._allele_counter}, "
-            f"count2cat={self._count2cat})"
-        )
-
-    def __repr__(self) -> str:
-        return str(self)
-
 
 class SexGenotypePredicate(GenotypePolyPredicate):
     # NOT PART OF THE PUBLIC API
@@ -729,7 +459,7 @@ def sex_predicate() -> GenotypePolyPredicate:
     """
     Get a genotype predicate for categorizing patients by their :class:`~gpsea.model.Sex`.
 
-    See the :ref:`male-female-predicate` section for an example.
+    See the :ref:`group-by-sex` section for an example.
     """
     return INSTANCE
 
@@ -829,11 +559,11 @@ def diagnosis_predicate(
     If an individual is diagnosed with more than one disease from the provided `diagnoses`,
     the individual will be assigned into no group (`None`).
 
-    See the :ref:`diagnosis-predicate` section for an example.
+    See the :ref:`group-by-diagnosis` section for an example.
 
-    :param diagnoses: an iterable with at least 2 diagnose IDs, either as a `str` or a :class:`~hpotk.TermId`
+    :param diagnoses: an iterable with at least 2 disease IDs, either as a `str` or a :class:`~hpotk.TermId`
       to determine the genotype group.
-    :param labels: an iterable with diagnose names or `None` if CURIEs should be used instead.
+    :param labels: an iterable with diagnose names or `None` if disease IDs should be used instead.
       The number of labels must match the number of predicates.
     """
     return DiagnosisPredicate.create(
diff --git a/src/gpsea/analysis/predicate/genotype/_predicates.py b/src/gpsea/analysis/predicate/genotype/_predicates.py
index b1ac93e6..31cea6f4 100644
--- a/src/gpsea/analysis/predicate/genotype/_predicates.py
+++ b/src/gpsea/analysis/predicate/genotype/_predicates.py
@@ -130,7 +130,7 @@ def __init__(self, symbol: str) -> None:
         self._symbol = symbol
 
     def get_question(self) -> str:
-        return f'impacts {self._symbol}'
+        return f'affects {self._symbol}'
 
     def test(self, variant: Variant) -> bool:
         for tx in variant.tx_annotations:
@@ -477,7 +477,8 @@ def __init__(
         self._tx_id = tx_id
         
     def get_question(self) -> str:
-        return f'variant affects aminoacid(s) between {self._region.start} and {self._region.end} ' \
+        # We report in 1-based coordinate system
+        return f'variant affects aminoacid(s) between {self._region.start + 1} and {self._region.end} ' \
             f'on protein encoded by transcript {self._tx_id}'
 
     def test(self, variant: Variant) -> bool:
diff --git a/src/gpsea/analysis/predicate/genotype/_variant.py b/src/gpsea/analysis/predicate/genotype/_variant.py
index a35b7e08..d9280554 100644
--- a/src/gpsea/analysis/predicate/genotype/_variant.py
+++ b/src/gpsea/analysis/predicate/genotype/_variant.py
@@ -42,7 +42,7 @@ def all(predicates: typing.Iterable[VariantPredicate]) -> VariantPredicate:
         >>> genes = ('SURF1', 'SURF2',)
         >>> predicate = VariantPredicates.all(VariantPredicates.gene(g) for g in genes)
         >>> predicate.get_question()
-        '(impacts SURF1 AND impacts SURF2)'
+        '(affects SURF1 AND affects SURF2)'
 
         Args:
             predicates: an iterable of predicates to test
@@ -171,15 +171,27 @@ def exon(
         return VariantExonPredicate(exon, tx_id)
 
     @staticmethod
-    def region(region: Region, tx_id: str) -> VariantPredicate:
+    def region(
+        region: typing.Union[Region, typing.Tuple[int, int]],
+        tx_id: str,
+    ) -> VariantPredicate:
         """
         Prepare a :class:`VariantPredicate` that tests if the variant
         overlaps with a region on a protein of a specific transcript.
 
         Args:
             region: a :class:`~gpsea.model.genome.Region` that gives the start and end coordinate
-                of the region of interest on a protein strand.
-        """
+                of the region of interest on a protein strand or a tuple with 1-based coordinates.
+        """
+        if isinstance(region, Region):
+            pass
+        elif isinstance(region, tuple) and len(region) == 2 and all(isinstance(c, int) and c > 0 for c in region):
+            start = region[0] - 1  # Convert to 0-based
+            end = region[1]
+            region = Region(start=start, end=end)
+        else:
+            raise ValueError(f'region must be a `Region` or a tuple with two positive `int`s, but got {region}')
+
         return ProteinRegionPredicate(region, tx_id)
 
     @staticmethod


Test patch:
diff --git a/src/gpsea/analysis/predicate/genotype/_test__gt_predicates.py b/src/gpsea/analysis/predicate/genotype/_test__gt_predicates.py
index 4c7f3d25..0eb50d0c 100644
--- a/src/gpsea/analysis/predicate/genotype/_test__gt_predicates.py
+++ b/src/gpsea/analysis/predicate/genotype/_test__gt_predicates.py
@@ -1,11 +1,7 @@
 import typing
 import pytest
 
-from ._gt_predicates import (
-    build_count_to_cat,
-    ModeOfInheritancePredicate,
-    ModeOfInheritanceInfo,
-)
+from ._gt_predicates import _build_count_to_cat, _build_ac_to_cat, _qc_partitions
 
 
 @pytest.mark.parametrize(
@@ -48,8 +44,9 @@ def test_build_count_to_cat(
     partitions: typing.Sequence[typing.Sequence[int]],
     ac2cat_name: typing.Mapping[typing.Tuple[int, int], str],
 ):
-    ac2cat = build_count_to_cat(
-        a_label="A", b_label="B",
+    ac2cat = _build_count_to_cat(
+        a_label="A",
+        b_label="B",
         partitions=partitions,
     )
 
@@ -59,45 +56,90 @@ def test_build_count_to_cat(
 
 
 @pytest.mark.parametrize(
-    "partitions, moi_data, ac2cat_name",
+    "partitions, ac2cat_name",
     [
         (
-            [(0,), (1,)],
-            ModeOfInheritanceInfo.autosomal_dominant(),
+            ((0,), (1,), (2,)),
+            {
+                0: "0",
+                1: "1",
+                2: "2",
+            },
+        ),
+        (
+            ((0, 1), (2,)),
             {
-                0: "No allele",
-                1: "Monoallelic",
+                0: "0 OR 1",
+                1: "0 OR 1",
+                2: "2",
             },
         ),
         (
-            [(0,), (1,), (2,)],
-            ModeOfInheritanceInfo.autosomal_recessive(),
+            ((0,), (1, 2)),
             {
-                0: "No allele",
-                1: "Monoallelic",
-                2: "Biallelic",
+                0: "0",
+                1: "1 OR 2",
+                2: "1 OR 2",
             },
         ),
         (
-            [(1,), (2,)],
-            ModeOfInheritanceInfo.autosomal_recessive(),
+            ((1,), (2,)),
             {
-                1: "Monoallelic",
-                2: "Biallelic",
+                1: "1",
+                2: "2",
             },
         ),
     ],
 )
-def test_prepare_count_to_cat(
+def test__build_ac_to_cat(
     partitions: typing.Sequence[typing.Sequence[int]],
-    moi_data: ModeOfInheritanceInfo,
     ac2cat_name: typing.Mapping[int, str],
 ):
-    ac2cat = ModeOfInheritancePredicate.prepare_count2cat(
+    ac2cat = _build_ac_to_cat(
         partitions=partitions,
-        mode_of_inheritance_data=moi_data,
     )
 
     for ac, expected_cat_name in ac2cat_name.items():
         categorization = ac2cat[ac]
         assert categorization.category.name == expected_cat_name
+
+
+@pytest.mark.parametrize(
+    "partitions, expected",
+    [
+        (
+            ((0, 1), (1,)),
+            "element 1 was present 2!=1 times",
+        ),
+        (((0,), (0,), (2,)),
+            "partition (0,) was present 2!=1 times"),
+        (
+            ((0, 1), (1, 2,)),
+            "element 1 was present 2!=1 times",
+        ),
+        (
+            ((0, 1), (1, 0)),
+            "element 0 was present 2!=1 times, element 1 was present 2!=1 times",
+        ),
+        (
+            ((0, 1, 1), (1, 1, 2,)),
+            "element 1 was present 4!=1 times",
+        ),
+        (
+            ((0.1,), (1,), (2,)),
+            "Each partition index must be a non-negative int",
+        ),
+        (
+            ((0, 1, 2),),
+            "At least 2 partitions must be provided",
+        ),
+    ],
+)
+def test__qc_partitions__explodes_when_expected(
+    partitions: typing.Sequence[typing.Sequence[int]],
+    expected: str,
+):
+    with pytest.raises(ValueError) as e:
+        _qc_partitions(partitions=partitions)
+
+    assert e.value.args == (expected,)
diff --git a/tests/analysis/predicate/genotype/test_gt_predicates.py b/tests/analysis/predicate/genotype/test_gt_predicates.py
index 704d956f..551d07f9 100644
--- a/tests/analysis/predicate/genotype/test_gt_predicates.py
+++ b/tests/analysis/predicate/genotype/test_gt_predicates.py
@@ -1,133 +1,63 @@
-import typing
 import pytest
 
 from gpsea.model import Patient, Sex, SampleLabels, VariantEffect
 from gpsea.analysis.predicate.genotype import (
-    GenotypePolyPredicate,
-    groups_predicate,
     sex_predicate,
     monoallelic_predicate,
     biallelic_predicate,
-    autosomal_dominant,
-    autosomal_recessive,
+    allele_count,
     VariantPredicates,
-    VariantPredicate,
 )
 
 
 TX_ID = "tx:xyz"
 
 
-class TestGroupsPredicate:
-
-    @pytest.fixture(scope="class")
-    def predicate(self) -> GenotypePolyPredicate:
-        return groups_predicate(
-            predicates=(
-                VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, TX_ID),
-                VariantPredicates.variant_effect(
-                    VariantEffect.FRAMESHIFT_VARIANT, TX_ID
-                ),
-            ),
-            group_names=(
-                "Point",
-                "LoF",
-            ),
-        )
-
-    def test_get_question(
-        self,
-        predicate: GenotypePolyPredicate,
-    ):
-        question = predicate.get_question_base()
-        assert question == "Genotype group"
-
-    def test_get_categorizations(
-        self,
-        predicate: GenotypePolyPredicate,
-    ):
-        categorizations = predicate.get_categorizations()
-
-        names = [c.category.name for c in categorizations]
-        assert names == ["Point", "LoF"]
-
-        descriptions = [c.category.description for c in categorizations]
-        assert descriptions == [
-            "MISSENSE_VARIANT on tx:xyz",
-            "FRAMESHIFT_VARIANT on tx:xyz",
-        ]
-
-    def test_test__missense(
-        self,
-        patient_w_missense: Patient,
-        predicate: GenotypePolyPredicate,
-    ):
-        cat = predicate.test(patient_w_missense)
-
-        assert cat is not None
-        assert cat.category.cat_id == 0
-        assert cat.category.name == "Point"
-        assert cat.category.description == "MISSENSE_VARIANT on tx:xyz"
-
-    def test_test__frameshift(
-        self,
-        patient_w_frameshift: Patient,
-        predicate: GenotypePolyPredicate,
-    ):
-        cat = predicate.test(patient_w_frameshift)
-
-        assert cat is not None
-        assert cat.category.cat_id == 1
-        assert cat.category.name == "LoF"
-        assert cat.category.description == "FRAMESHIFT_VARIANT on tx:xyz"
-
-
-class TestModeOfInheritancePredicate:
-
-    @pytest.fixture(scope="class")
-    def variant_predicate(self) -> VariantPredicate:
-        return VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, TX_ID)
+class TestAlleleCount:
 
     @pytest.mark.parametrize(
         "patient_name,name",
         [
-            ("adam", "No allele"),
-            ("eve", "Monoallelic"),
-            ("cain", "Monoallelic"),
+            ("adam", "1"),
+            ("eve", "1"),
+            ("cain", "1"),
         ],
     )
-    def test_autosomal_dominant(
+    def test_ad_family__all_variants(
         self,
         patient_name: str,
         name: str,
-        variant_predicate: VariantPredicate,
         request: pytest.FixtureRequest,
     ):
         patient = request.getfixturevalue(patient_name)
-        predicate = autosomal_dominant(variant_predicate)
+        predicate = allele_count(counts=((0,), (1,)))
 
         categorization = predicate.test(patient)
 
         assert categorization is not None
 
         assert categorization.category.name == name
-
+    
     @pytest.mark.parametrize(
         "patient_name,name",
         [
-            ("adam", "Monoallelic"),  # 0/0 & 0/1
-            ("eve", "Monoallelic"),  # 0/1 & 0/0
-            ("cain", "Monoallelic"),  # 0/1 & 0/0
+            ("adam", "0"),
+            ("eve", "1"),
+            ("cain", "1"),
         ],
     )
-    def test_autosomal_dominant__with_default_predicate(
+    def test_ad_family__missense_subset(
         self,
         patient_name: str,
         name: str,
         request: pytest.FixtureRequest,
     ):
+        is_missense = VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, tx_id=TX_ID)
         patient = request.getfixturevalue(patient_name)
-        predicate = autosomal_dominant()
+        predicate = allele_count(
+            counts=((0,), (1,)),
+            target=is_missense,
+        )
 
         categorization = predicate.test(patient)
 
@@ -138,46 +68,48 @@ def test_autosomal_dominant__with_default_predicate(
     @pytest.mark.parametrize(
         "patient_name,name",
         [
-            ("walt", "Monoallelic"),
-            ("skyler", "Monoallelic"),
-            ("flynn", "Biallelic"),
-            ("holly", "No allele"),
+            ("walt", "2"),
+            ("skyler", "2"),
+            ("flynn", "2"),
+            ("holly", "2"),
         ],
     )
-    def test_autosomal_recessive(
+    def test_ar_family__all_variants(
         self,
         patient_name: str,
         name: str,
-        variant_predicate: VariantPredicate,
         request: pytest.FixtureRequest,
     ):
         patient = request.getfixturevalue(patient_name)
-        predicate = autosomal_recessive(variant_predicate)
+        predicate = allele_count(counts=((0, 1), (2,)))
 
         categorization = predicate.test(patient)
 
         assert categorization is not None
 
         assert categorization.category.name == name
-
+    
     @pytest.mark.parametrize(
         "patient_name,name",
         [
-            # The White family has two variants:
-            ("walt", "Biallelic"),  # 0/1 & 0/1
-            ("skyler", "Biallelic"),  # 0/1 & 0/1
-            ("flynn", "Biallelic"),  # 1/1 & 0/0
-            ("holly", "Biallelic"),  # 0/0 & 1/1
+            ("walt", "1"),
+            ("skyler", "1"),
+            ("flynn", "2"),
+            ("holly", "0"),
         ],
     )
-    def test_autosomal_recessive__with_default_predicate(
+    def test_ar_family__only_missense(
         self,
         patient_name: str,
         name: str,
         request: pytest.FixtureRequest,
     ):
         patient = request.getfixturevalue(patient_name)
-        predicate = autosomal_recessive()
+        is_missense = VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, tx_id=TX_ID)
+        predicate = allele_count(
+            counts=((0,), (1,), (2,)),
+            target=is_missense,
+        )
 
         categorization = predicate.test(patient)
 
@@ -185,6 +117,18 @@ def test_autosomal_recessive__with_default_predicate(
 
         assert categorization.category.name == name
 
+    def test_eq_and_hash(self):
+        a = allele_count(counts=((0, 1), (2,)))
+        b = allele_count(counts=((0, 1), (2,)))
+
+        assert a == b
+        assert hash(a) == hash(b)
+
+    def test_display_question(self):
+        a = allele_count(counts=((0, 1), (2,)))
+
+        assert a.display_question() == "Allele count: 0 OR 1, 2"
+
 
 class TestAllelePredicates:
 
@@ -257,35 +201,6 @@ def test_biallelic_predicate__general_stuff(
         
         assert gt_predicate.display_question() == 'Allele group: A/A, A/B, B/B'
 
-    @pytest.mark.parametrize(
-        "partitions, msg",
-        [
-            (((0,), (0,), (2,)), "partition (0,) was present 2!=1 times"),
-            (((0, 1), (1, 2,)), "element 1 was present 2!=1 times"),
-            (((0, 1), (1, 0)), "element 0 was present 2!=1 times, element 1 was present 2!=1 times"),
-            (((0, 1, 1), (1, 1, 2,)), "element 1 was present 4!=1 times"),
-            (((0.1,), (1,), (2,)), "Each partition index must be a non-negative int"),
-            (((0, 1, 2),), "At least 2 partitions must be provided"),
-        ]
-    )
-    def test_biallelic_predicate__invalid_partitions(
-        self,
-        partitions: typing.Sequence[typing.Sequence[int]],
-        msg: str,
-    ):
-        is_missense = VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, TX_ID)
-        is_synonymous = VariantPredicates.variant_effect(VariantEffect.SYNONYMOUS_VARIANT, TX_ID)
-        
-        with pytest.raises(ValueError) as e:
-            biallelic_predicate(
-                a_predicate=is_missense,
-                b_predicate=is_synonymous,
-                a_label="A", b_label="B",
-                partitions=partitions
-            )
-
-        assert e.value.args == (msg,)
-
 
 class TestSexPredicate:
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 0ddf7393..9fab5b9b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,10 +9,30 @@
 
 from gpsea.analysis.mtc_filter import PhenotypeMtcResult
 from gpsea.analysis.pcats import HpoTermAnalysisResult
-from gpsea.analysis.predicate.genotype import GenotypePolyPredicate, VariantPredicates, autosomal_dominant
+from gpsea.analysis.predicate.genotype import (
+    GenotypePolyPredicate,
+    VariantPredicates,
+    allele_count,
+)
 from gpsea.analysis.predicate.phenotype import PhenotypePolyPredicate, HpoPredicate
 from gpsea.io import GpseaJSONDecoder
-from gpsea.model import *
+from gpsea.model import (
+    Cohort,
+    Disease,
+    Genotype,
+    Genotypes,
+    Patient,
+    Phenotype,
+    ProteinMetadata,
+    SampleLabels,
+    Sex,
+    TranscriptAnnotation,
+    TranscriptCoordinates,
+    Variant,
+    VariantCoordinates,
+    VariantEffect,
+    VariantInfo,
+)
 from gpsea.model.genome import GRCh38, GenomicRegion, Region, Strand, GenomeBuild
 from ._protein_test_service import ProteinTestMetadataService
 
@@ -24,7 +44,9 @@ def pytest_addoption(parser):
 
 
 def pytest_configure(config):
-    config.addinivalue_line("markers", "online: mark test that require internet access to run")
+    config.addinivalue_line(
+        "markers", "online: mark test that require internet access to run"
+    )
 
 
 def pytest_collection_modifyitems(config, items):
@@ -37,7 +59,7 @@ def pytest_collection_modifyitems(config, items):
             item.add_marker(skip_online)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_project_dir(fpath_test_dir: str) -> str:
     """
     Path to project folder, where `pyproject.toml`, `README.md`,
@@ -46,7 +68,7 @@ def fpath_project_dir(fpath_test_dir: str) -> str:
     return os.path.dirname(fpath_test_dir)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_test_dir() -> str:
     """
     Path to `tests` folder.
@@ -54,7 +76,7 @@ def fpath_test_dir() -> str:
     return os.path.dirname(os.path.abspath(__file__))
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_docs_dir(fpath_project_dir: str) -> str:
     """
     Path to `docs` folder.
@@ -62,43 +84,43 @@ def fpath_docs_dir(fpath_project_dir: str) -> str:
     return os.path.join(fpath_project_dir, "docs")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_cohort_data_dir(fpath_docs_dir: str) -> str:
     return os.path.join(fpath_docs_dir, "cohort-data")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_test_data_dir(fpath_test_dir: str) -> str:
-    return os.path.join(fpath_test_dir, 'test_data')
+    return os.path.join(fpath_test_dir, "test_data")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_phenopacket_dir(fpath_test_data_dir: str) -> str:
-    return os.path.join(fpath_test_data_dir, 'phenopackets')
+    return os.path.join(fpath_test_data_dir, "phenopackets")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_toy_hpo(fpath_test_data_dir: str) -> str:
-    return os.path.join(fpath_test_data_dir, 'hp.toy.json')
+    return os.path.join(fpath_test_data_dir, "hp.toy.json")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def toy_hpo(fpath_toy_hpo: str) -> hpotk.MinimalOntology:
     return hpotk.load_minimal_ontology(fpath_toy_hpo)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def hpo(fpath_test_data_dir: str) -> hpotk.MinimalOntology:
-    fpath_hpo = os.path.join(fpath_test_data_dir, 'hp.v2024-04-26.json.gz')
+    fpath_hpo = os.path.join(fpath_test_data_dir, "hp.v2024-04-26.json.gz")
     return hpotk.load_minimal_ontology(fpath_hpo)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def validation_runner(hpo: hpotk.MinimalOntology) -> hpotk.validate.ValidationRunner:
     validators = (
         hpotk.validate.ObsoleteTermIdsValidator(hpo),
         hpotk.validate.AnnotationPropagationValidator(hpo),
-        hpotk.validate.PhenotypicAbnormalityValidator(hpo)
+        hpotk.validate.PhenotypicAbnormalityValidator(hpo),
     )
     return hpotk.validate.ValidationRunner(validators)
 
@@ -109,46 +131,46 @@ def make_region(contig: str, start: int, end: int) -> GenomicRegion:
     return GenomicRegion(a_contig, start, end, Strand.POSITIVE)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def protein_test_service() -> ProteinTestMetadataService:
     return ProteinTestMetadataService.create()
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_suox_cohort(
-        fpath_test_data_dir: str,
+    fpath_test_data_dir: str,
 ) -> str:
-    return os.path.join(fpath_test_data_dir, 'SUOX.json')
+    return os.path.join(fpath_test_data_dir, "SUOX.json")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_cohort(
-        fpath_suox_cohort: str,
+    fpath_suox_cohort: str,
 ) -> Cohort:
     with open(fpath_suox_cohort) as fh:
         return json.load(fh, cls=GpseaJSONDecoder)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_mane_tx_id() -> str:
-    return 'NM_001032386.2'
+    return "NM_001032386.2"
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_gt_predicate(
     suox_mane_tx_id: str,
 ) -> GenotypePolyPredicate:
-    return autosomal_dominant(
-        variant_predicate=VariantPredicates.variant_effect(
-            effect=VariantEffect.MISSENSE_VARIANT,
-            tx_id=suox_mane_tx_id
-        )
+    return allele_count(
+        counts=((0,), (1,)),
+        target=VariantPredicates.variant_effect(
+            effect=VariantEffect.MISSENSE_VARIANT, tx_id=suox_mane_tx_id
+        ),
     )
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_pheno_predicates(
-        hpo: hpotk.MinimalOntology,
+    hpo: hpotk.MinimalOntology,
 ) -> typing.Sequence[PhenotypePolyPredicate[hpotk.TermId]]:
     """
     Get predicates for test for presence of 5 HPO terms:
@@ -159,23 +181,23 @@ def suox_pheno_predicates(
     return (
         HpoPredicate(
             hpo=hpo,
-            query=hpotk.TermId.from_curie('HP:0001250'),  # Seizure
+            query=hpotk.TermId.from_curie("HP:0001250"),  # Seizure
         ),
         HpoPredicate(
             hpo=hpo,
-            query=hpotk.TermId.from_curie('HP:0001083'),  # Ectopia lentis
+            query=hpotk.TermId.from_curie("HP:0001083"),  # Ectopia lentis
         ),
         HpoPredicate(
             hpo=hpo,
-            query=hpotk.TermId.from_curie('HP:0032350'),  # Sulfocysteinuria
+            query=hpotk.TermId.from_curie("HP:0032350"),  # Sulfocysteinuria
         ),
         HpoPredicate(
             hpo=hpo,
-            query=hpotk.TermId.from_curie('HP:0012758'),  # Neurodevelopmental delay
+            query=hpotk.TermId.from_curie("HP:0012758"),  # Neurodevelopmental delay
         ),
         HpoPredicate(
             hpo=hpo,
-            query=hpotk.TermId.from_curie('HP:0001276'),  # Hypertonia
+            query=hpotk.TermId.from_curie("HP:0001276"),  # Hypertonia
         ),
     )
 
@@ -231,217 +253,351 @@ def make_count_df(
     )
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_suox_tx_coordinates(fpath_test_data_dir: str) -> str:
-    suox_mane_tx_id = 'NM_001032386.2'
-    return os.path.join(fpath_test_data_dir, f'SUOX-{suox_mane_tx_id}.json')
+    suox_mane_tx_id = "NM_001032386.2"
+    return os.path.join(fpath_test_data_dir, f"SUOX-{suox_mane_tx_id}.json")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_mane_tx_coordinates(
-        fpath_suox_tx_coordinates: str,
+    fpath_suox_tx_coordinates: str,
 ) -> TranscriptCoordinates:
     with open(fpath_suox_tx_coordinates) as fh:
         return json.load(fh, cls=GpseaJSONDecoder)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def fpath_suox_protein_metadata(fpath_test_data_dir: str) -> str:
-    suox_mane_tx_protein_id = 'NP_001027558.1'
-    return os.path.join(fpath_test_data_dir, f'SUOX-{suox_mane_tx_protein_id}.json')
+    suox_mane_tx_protein_id = "NP_001027558.1"
+    return os.path.join(fpath_test_data_dir, f"SUOX-{suox_mane_tx_protein_id}.json")
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def suox_protein_metadata(
-        fpath_suox_protein_metadata: str,
+    fpath_suox_protein_metadata: str,
 ) -> ProteinMetadata:
     with open(fpath_suox_protein_metadata) as fh:
         return json.load(fh, cls=GpseaJSONDecoder)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def toy_cohort(
-        test_phenotypes: typing.Mapping[str, Phenotype],
-        test_diseases: typing.Mapping[str, Disease],
+    test_phenotypes: typing.Mapping[str, Phenotype],
+    test_diseases: typing.Mapping[str, Disease],
 ) -> Cohort:
-    prot_id = 'NP_037407.4'
+    prot_id = "NP_037407.4"
 
     dup = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89279849, 89279850), ref='G', alt='GC', change_length=1)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89279849, 89279850),
+                ref="G",
+                alt="GC",
+                change_length=1,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.6691dup', False, [VariantEffect.FRAMESHIFT_VARIANT], [9],
-                prot_id, "NP_001243112.1:p.Ala2231GlyfsTer29", Region(2230, 2231))
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.6691dup",
+                False,
+                [VariantEffect.FRAMESHIFT_VARIANT],
+                [9],
+                prot_id,
+                "NP_001243112.1:p.Ala2231GlyfsTer29",
+                Region(2230, 2231),
+            )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HetSingleVar'): Genotype.HETEROZYGOUS}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HetSingleVar"): Genotype.HETEROZYGOUS}
+        ),
     )
     indel = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89284600, 89284602), ref='GG', alt='A', change_length=-1),
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89284600, 89284602),
+                ref="GG",
+                alt="A",
+                change_length=-1,
+            ),
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.1940_1941delinsT', False, [VariantEffect.FRAMESHIFT_VARIANT], [9], 
-                prot_id, "NP_001243111.1:p.Ser647LeufsTer6", Region(646, 647))
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.1940_1941delinsT",
+                False,
+                [VariantEffect.FRAMESHIFT_VARIANT],
+                [9],
+                prot_id,
+                "NP_001243111.1:p.Ser647LeufsTer6",
+                Region(646, 647),
+            )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HetDoubleVar1'): Genotype.HETEROZYGOUS}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HetDoubleVar1"): Genotype.HETEROZYGOUS}
+        ),
     )
     snv_stop_gain = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89280751, 89280752), ref='G', alt='T', change_length=0)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89280751, 89280752),
+                ref="G",
+                alt="T",
+                change_length=0,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.5790C>A', False, [VariantEffect.STOP_GAINED], [9], 
-                prot_id, "NP_037407.4:p.Tyr1930Ter", Region(1929, 1930))
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.5790C>A",
+                False,
+                [VariantEffect.STOP_GAINED],
+                [9],
+                prot_id,
+                "NP_037407.4:p.Tyr1930Ter",
+                Region(1929, 1930),
+            )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HetDoubleVar1'): Genotype.HETEROZYGOUS}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HetDoubleVar1"): Genotype.HETEROZYGOUS}
+        ),
     )
     snv_missense = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89275127, 89275128), ref='G', alt='A', change_length=0)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89275127, 89275128),
+                ref="G",
+                alt="A",
+                change_length=0,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.7534C>T', False, [VariantEffect.MISSENSE_VARIANT], [10], 
-                prot_id, "NP_037407.4:p.Arg2512Trp", Region(2511, 2512))
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.7534C>T",
+                False,
+                [VariantEffect.MISSENSE_VARIANT],
+                [10],
+                prot_id,
+                "NP_037407.4:p.Arg2512Trp",
+                Region(2511, 2512),
+            )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HetDoubleVar2'): Genotype.HETEROZYGOUS}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HetDoubleVar2"): Genotype.HETEROZYGOUS}
+        ),
     )
     del_frameshift = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89279707, 89279725), ref='AGTGTTCGGGGCGGGGCC', alt='A', change_length=-17)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89279707, 89279725),
+                ref="AGTGTTCGGGGCGGGGCC",
+                alt="A",
+                change_length=-17,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.6817_6833del', False, [VariantEffect.FRAMESHIFT_VARIANT], [9], 
-                prot_id, "NP_037407.4:p.Gly2273CysfsTer17", Region(2272, 2278),
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.6817_6833del",
+                False,
+                [VariantEffect.FRAMESHIFT_VARIANT],
+                [9],
+                prot_id,
+                "NP_037407.4:p.Gly2273CysfsTer17",
+                Region(2272, 2278),
             )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HetDoubleVar2'): Genotype.HETEROZYGOUS}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HetDoubleVar2"): Genotype.HETEROZYGOUS}
+        ),
     )
     del_small = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89279457, 89279459), ref='TG', alt='T', change_length=-1)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89279457, 89279459),
+                ref="TG",
+                alt="T",
+                change_length=-1,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', 'NM_013275.6:c.7083del', False, [VariantEffect.FRAMESHIFT_VARIANT], [9],
-                prot_id, "NP_037407.4:p.Thr2362ProfsTer39", Region(2360, 2362))
+                "ANKRD11",
+                "NM_013275.6",
+                "NM_013275.6:c.7083del",
+                False,
+                [VariantEffect.FRAMESHIFT_VARIANT],
+                [9],
+                prot_id,
+                "NP_037407.4:p.Thr2362ProfsTer39",
+                Region(2360, 2362),
+            )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('HomoVar'): Genotype.HOMOZYGOUS_ALTERNATE}),
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("HomoVar"): Genotype.HOMOZYGOUS_ALTERNATE}
+        ),
     )
     del_large = Variant(
         variant_info=VariantInfo(
-            variant_coordinates=VariantCoordinates(make_region("16", 89_190_070, 89_439_815), ref='N', alt='<DEL>', change_length=-249_745)
+            variant_coordinates=VariantCoordinates(
+                make_region("16", 89_190_070, 89_439_815),
+                ref="N",
+                alt="<DEL>",
+                change_length=-249_745,
+            )
         ),
         tx_annotations=[
             TranscriptAnnotation(
-                'ANKRD11', 'NM_013275.6', None, False, 
-                [VariantEffect.STOP_LOST, VariantEffect.FEATURE_TRUNCATION, VariantEffect.CODING_SEQUENCE_VARIANT, VariantEffect.FIVE_PRIME_UTR_VARIANT, VariantEffect.THREE_PRIME_UTR_VARIANT, VariantEffect.INTRON_VARIANT], 
+                "ANKRD11",
+                "NM_013275.6",
+                None,
+                False,
+                [
+                    VariantEffect.STOP_LOST,
+                    VariantEffect.FEATURE_TRUNCATION,
+                    VariantEffect.CODING_SEQUENCE_VARIANT,
+                    VariantEffect.FIVE_PRIME_UTR_VARIANT,
+                    VariantEffect.THREE_PRIME_UTR_VARIANT,
+                    VariantEffect.INTRON_VARIANT,
+                ],
                 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
-                prot_id, None, None
+                prot_id,
+                None,
+                None,
             )
         ],
-        genotypes=Genotypes.from_mapping({SampleLabels('LargeCNV'): Genotype.HETEROZYGOUS}))
+        genotypes=Genotypes.from_mapping(
+            {SampleLabels("LargeCNV"): Genotype.HETEROZYGOUS}
+        ),
+    )
 
     patients = (
         Patient.from_raw_parts(
-            SampleLabels('HetSingleVar'),
+            SampleLabels("HetSingleVar"),
             sex=Sex.UNKNOWN_SEX,
             age=None,
             vital_status=None,
             phenotypes=(
-                test_phenotypes['arachnodactyly_T'],
-                test_phenotypes['spasticity_F'],
-                test_phenotypes['focal_clonic_seizure_T']
+                test_phenotypes["arachnodactyly_T"],
+                test_phenotypes["spasticity_F"],
+                test_phenotypes["focal_clonic_seizure_T"],
             ),
             measurements=(),
             variants=(dup,),
-            diseases=(test_diseases['KBG_T'],)
+            diseases=(test_diseases["KBG_T"],),
         ),
         Patient.from_raw_parts(
-            SampleLabels('HetDoubleVar1'),
+            SampleLabels("HetDoubleVar1"),
             sex=Sex.UNKNOWN_SEX,
             age=None,
             vital_status=None,
             phenotypes=(
-                test_phenotypes['arachnodactyly_T'], test_phenotypes['seizure_T'], test_phenotypes['spasticity_T'],
+                test_phenotypes["arachnodactyly_T"],
+                test_phenotypes["seizure_T"],
+                test_phenotypes["spasticity_T"],
             ),
             measurements=(),
             variants=(indel, snv_stop_gain),
-            diseases=(test_diseases['KBG_T'],)
+            diseases=(test_diseases["KBG_T"],),
         ),
         Patient.from_raw_parts(
-            SampleLabels('HetDoubleVar2'),
+            SampleLabels("HetDoubleVar2"),
             sex=Sex.UNKNOWN_SEX,
             age=None,
             vital_status=None,
             phenotypes=(
-                test_phenotypes['arachnodactyly_F'], test_phenotypes['spasticity_T'], test_phenotypes['seizure_T'],
+                test_phenotypes["arachnodactyly_F"],
+                test_phenotypes["spasticity_T"],
+                test_phenotypes["seizure_T"],
             ),
             measurements=(),
             variants=(snv_missense, del_frameshift),
-            diseases=(test_diseases['KBG_T'],)
+            diseases=(test_diseases["KBG_T"],),
         ),
         Patient.from_raw_parts(
-            SampleLabels('HomoVar'),
+            SampleLabels("HomoVar"),
             sex=Sex.UNKNOWN_SEX,
             age=None,
             vital_status=None,
             phenotypes=(
-                test_phenotypes['arachnodactyly_T'], test_phenotypes['spasticity_T'], test_phenotypes['seizure_T'],
+                test_phenotypes["arachnodactyly_T"],
+                test_phenotypes["spasticity_T"],
+                test_phenotypes["seizure_T"],
             ),
             measurements=(),
             variants=(del_small,),
-            diseases=()
+            diseases=(),
         ),
         Patient.from_raw_parts(
-            SampleLabels('LargeCNV'),
+            SampleLabels("LargeCNV"),
             sex=Sex.UNKNOWN_SEX,
             age=None,
             vital_status=None,
             phenotypes=(
-                test_phenotypes['arachnodactyly_T'], test_phenotypes['spasticity_T'], test_phenotypes['seizure_F'],
+                test_phenotypes["arachnodactyly_T"],
+                test_phenotypes["spasticity_T"],
+                test_phenotypes["seizure_F"],
             ),
             measurements=(),
             variants=(del_large,),
-            diseases=()
+            diseases=(),
         ),
     )
 
     return Cohort.from_patients(patients)
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def test_phenotypes() -> typing.Mapping[str, Phenotype]:
     return {
-        'arachnodactyly_T': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001166'), True),
-        'seizure_T': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001250'), True),
-        'focal_clonic_seizure_T': Phenotype.from_raw_parts(
-            hpotk.TermId.from_curie('HP:0002266'), True,
+        "arachnodactyly_T": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001166"), True
         ),
-        'spasticity_T': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001257'), True),
-
-        'arachnodactyly_F': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001166'), False),
-        'seizure_F': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001250'), False),
-        'spasticity_F': Phenotype.from_raw_parts(hpotk.TermId.from_curie('HP:0001257'), False),
-        'focal_clonic_seizure_F': Phenotype.from_raw_parts(
-            hpotk.TermId.from_curie('HP:0002266'), False,
+        "seizure_T": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001250"), True
+        ),
+        "focal_clonic_seizure_T": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0002266"),
+            True,
+        ),
+        "spasticity_T": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001257"), True
+        ),
+        "arachnodactyly_F": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001166"), False
+        ),
+        "seizure_F": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001250"), False
+        ),
+        "spasticity_F": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0001257"), False
+        ),
+        "focal_clonic_seizure_F": Phenotype.from_raw_parts(
+            hpotk.TermId.from_curie("HP:0002266"),
+            False,
         ),
     }
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def test_diseases() -> typing.Mapping[str, Disease]:
     return {
-        'KBG_T': Disease.from_raw_parts(hpotk.TermId.from_curie("OMIM:148050"), "KBG syndrome", True),
-        'KBG_F': Disease.from_raw_parts(hpotk.TermId.from_curie("OMIM:148050"), "KBG syndrome", False),
+        "KBG_T": Disease.from_raw_parts(
+            hpotk.TermId.from_curie("OMIM:148050"), "KBG syndrome", True
+        ),
+        "KBG_F": Disease.from_raw_parts(
+            hpotk.TermId.from_curie("OMIM:148050"), "KBG syndrome", False
+        ),
     }
 
 
-@pytest.fixture(scope='session')
+@pytest.fixture(scope="session")
 def genome_build() -> GenomeBuild:
     return GRCh38
diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py
deleted file mode 100644
index b592f1f8..00000000
--- a/tests/test_tutorial.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import pytest
-import ppktstore
-import hpotk
-
-from gpsea.model import Cohort, VariantEffect
-from gpsea.preprocessing import configure_caching_cohort_creator, CohortCreator, load_phenopackets
-from gpsea.analysis.pcats import HpoTermAnalysis
-from gpsea.analysis.mtc_filter import HpoMtcFilter
-from gpsea.analysis.pcats.stats import FisherExactTest
-from gpsea.analysis.predicate.genotype import VariantPredicates, groups_predicate
-from gpsea.analysis.predicate.phenotype import prepare_predicates_for_terms_of_interest
-
-
-class TestTutorial:
-
-    @pytest.fixture(scope='class')
-    def cohort_creator(
-        self,
-        hpo: hpotk.MinimalOntology,
-    ) -> CohortCreator:
-        return configure_caching_cohort_creator(
-            hpo=hpo,
-        )
-
-    @pytest.fixture(scope='class')
-    def cohort(
-        self,
-        cohort_creator: CohortCreator,
-    ) -> Cohort:
-        registry = ppktstore.registry.configure_phenopacket_registry()
-        with registry.open_phenopacket_store('0.1.18') as ps:
-            phenopackets = tuple(ps.iter_cohort_phenopackets('TBX5'))
-
-        cohort, _ = load_phenopackets(
-            phenopackets=phenopackets,
-            cohort_creator=cohort_creator,
-        )
-        return cohort
-
-    @pytest.fixture
-    def mtc_filter(
-        self,
-        hpo: hpotk.MinimalOntology,
-    ) -> HpoMtcFilter:
-        return HpoMtcFilter.default_filter(
-            hpo=hpo,
-            term_frequency_threshold=0.2,
-        )
-
-    @pytest.fixture
-    def hpo_term_analysis(
-        self,
-        mtc_filter,
-    ) -> HpoTermAnalysis:
-        return HpoTermAnalysis(
-            count_statistic=FisherExactTest(),
-            mtc_filter=mtc_filter,
-            mtc_correction='fdr_bh',
-            mtc_alpha=0.05,
-        )
-
-    @pytest.mark.skip('Just for manual debugging')
-    def test_compare_genotype_vs_phenotype(
-        self,
-        hpo: hpotk.MinimalOntology,
-        cohort: Cohort,
-        hpo_term_analysis: HpoTermAnalysis,
-    ):
-        tx_id = 'NM_181486.4'
-
-        gt_predicate = groups_predicate(
-            predicates=(
-                VariantPredicates.variant_effect(VariantEffect.MISSENSE_VARIANT, tx_id),
-                VariantPredicates.variant_effect(VariantEffect.FRAMESHIFT_VARIANT, tx_id),
-            ),
-            group_names=('Missense', 'Frameshift',),
-        )
-        pheno_predicates = prepare_predicates_for_terms_of_interest(
-            cohort=cohort,
-            hpo=hpo,
-            min_n_of_patients_with_term=2,
-        )
-
-        result = hpo_term_analysis.compare_genotype_vs_phenotypes(
-            cohort=cohort,
-            gt_predicate=gt_predicate,
-            pheno_predicates=pheno_predicates,
-        )
-
-        assert result is not None