Skip to content

Ordinal "at least / at most" conditions (opt-in ordinal_conditions) - #45

Merged
Fuminides merged 2 commits into
mainfrom
feature/ordinal-conditions
Nov 10, 2026
Merged

Fuminides merged 2 commits into
mainfrom
feature/ordinal-conditions

Conversation

@Fuminides

@Fuminides Fuminides commented Nov 10, 2026 •

Copy link
Copy Markdown
Owner

What

ordinal_conditions=True (default False) lets a rule state x IS at least t or x IS at most t over the ordered terms of a variable, besides a single term:

IF no2_last IS at least Fair AND humidity IS at most Medium WITH DS 0.41, ACC 0.83

Supported on BaseFuzzyRulesClassifier (winning-rule, inference='mixture', mixture_search) and BaseFuzzyRulesRegressor (crisp and Mamdani), and on the problems behind them (FitRuleBase, FitRuleBaseRegression). Docs: docs/source/ordinal_conditions.rst; changelog under "Unreleased". No version bump, no tag.

Update: per-variable selection and named compound terms (commit 7a0844a)

After seeing local_hour IS at most Medium on a cyclic clock variable:

  • ordinal_conditions accepts True, False (default) or a collection of features, by name or index (['temp', 'level'], [1, 2]). Only those variables get at least / at most; the others keep single terms. A feature named explicitly must be able to have them (3+ terms, not categorical) or a ValueError says so; unknown names and bad indexes are errors; a bare string or dict is rejected. Empty/False/None are off (the problem is then built with exactly the 3.4.0 arguments, tested).
  • compound_terms (new, default None): {feature: [(name, [term, ...]), ...]}, feature by name or index, terms by name or index. Each entry adds one condition x IS <name> whose membership is the bounded sum of the listed terms. The terms need not be contiguous, so ('Night', ['Very High', 'Low']) wraps around midnight; on a linear axis that union is two shoulders, one at each end (documented and tested with values). The set is stored sorted.
  • Validation (ValueError): at least 2 distinct terms; not every term; unknown term / index out of range; no two compounds with the same set; no compound with the set of an ordinal condition the variable has; the name must be unique within its variable (not a term name, another compound's name or an ordinal condition's name; non-empty, no ;, line breaks, parentheses, AND/IS/WITH/THEN, not starting with at least/at most); a feature given twice.
  • Integration: same machinery as the ordinal conditions: codes after the singles and the ordinal ones, rows of compute_memberships, the packed gather table, the remap (n_term_codes), canonical codes, and the array, batch_size, population and PyTorch objectives (the latter declines for searched partitions). Works with or without ordinal conditions, with fixed or searched partitions, T1 and interval T2, also on categorical variables. Same estimators (BaseFuzzyRulesClassifier incl. mixture and mixture_search, BaseFuzzyRulesRegressor crisp/fuzzy, FitRuleBase, FitRuleBaseRegression); refused with candidate_rules and for general Type-2.
  • Reading: rules print IF local_hour IS Night. Condition is now (feature, variable, operator, term, term_index, terms, code, name); for 'any_of', term and term_index are None, terms lists the constituent term names (low to high) and name is the given name; for the other operators name is None. fuzzyVariable.condition(code) gives ('any_of', position in variable.compound); condition_code('any_of', name) is the inverse.
  • Persistence: rule text round-trips given variables that carry the compound (load_fuzzy_variables reads one line Compound;<name>;<term indexes> per compound, written only when there are any); pickles, copies, pattern-stability keys, rules_to_latex and the charts handle it.
  • Tests (118 in tests/test_ordinal_conditions.py): round trips, every validation error, wrap-around membership values, evaluator equivalence (array = object oracle for fixed/searched, T1/T2, with and without ordinal/batch_size; population and torch), unchanged default fits (the recorded 3.4.0 fixture and a spy that the problem receives no new argument).

Design

  • Conditions. A variable with n ordered terms offers the n single terms, at least t_j and at most t_j for j = 1..n-2: 3n - 4 conditions (8 for 4 terms). at least t_0 (= don't care), at least t_(n-1) (= t_(n-1)) and their at most twins are not offered; no general intervals. No two conditions cover the same set, so a set has exactly one code.
  • Membership. Bounded sum min(1, sum of covered terms). On a strong partition it is the exact complement of the other terms (a shouldered trapezoid); the default construct_partitions trapezoids and construct_breakpoint_partition are strong (checked). Gaussian terms are not strong and saturate at 1; interval Type-2 sums the lower and upper bounds apart; general Type-2 is refused; categorical variables keep single terms. With the default product t-norm a condition is one antecedent in [0, 1], multiplied like a single term. The bounded sum is the dual of the Lukasiewicz t-norm, not of the product (documented).
  • Encoding. The term gene is widened to the condition codes, no gene is added: bounds are [-1, max_codes - 1] and the existing proportional remap (_fitness.remap_term) is applied to each variable's code count (FitRuleBase.n_term_codes, equal to the term counts when off). RuleSimple.antecedents stores the same code (terms first, then the at least codes, then the at most ones), so rule equality, hashing, delete_rule_duplicates, the array decoder's per-class dedup and the mixture's duplicate detection work unchanged. Sampling, SBX and polynomial mutation work on the bounds. The extra conditions are extra rows of fuzzyVariable.compute_memberships (and columns of the packed gather table), so the array, streamed (batch_size), population and PyTorch (fixed partitions; it declines for searched partitions) objectives handle them; all agree with the object decoder bit for bit (tests).
  • Reading. rules.rule_conditions(rule, antecedents) / rule.conditions(antecedents) return Condition(feature, variable, operator, term, term_index, terms, code), operator in {'is', 'at_least', 'at_most'}, no text parsing. Printing says IS at least Fair / IS at most Fair (rules, print_mixture, regression rules, rules_to_latex, pattern-stability reports and charts, rule keys).
  • Persistence. Rule text round-trips (the loader reads IS at least X, swapping an unflagged variable for a flagged copy in the returned rule base, never mutating the input); save_fuzzy_variables writes one extra line Ordinal conditions only for variables that have them (the others are byte-identical); pickles round-trip and old pickles (variable, classifier, regressor) load with the option off.

Backward compatibility (off = 3.4.0)

tests/data/ordinal_default_golden.json was recorded on main (10d811b) before any change: genes, rule text, prediction digests, objective, variable text and rule-text round trip of nine fits (searched T1 and T2 partitions, fixed partitions with ds_mode=2, mixture_search, three regression modes). test_default_fits_equal_the_recorded_release_fits replays them on this branch and requires equality. The default bounds and n_term_codes == n_lv_possible are asserted separately, and the option is only passed to the problem when on. I also ran the other 100+ test files one at a time (all pass).

Small check (benchmarks/benchmark_ordinal_conditions.py)

Fixed 4-term strong partitions from the training fold, 20 rules, up to 3 conditions, 60 generations x 50 individuals (mixture arm: 20 generations, mixture_search, non-negative), 5-fold x 2 seeds (mixture: 1 seed). Mean over the held-out folds; on-2x doubles the generations. Macro F1 (R^2 for diabetes), rules kept, mean conditions per rule, fit time.

data arm score rules cond/rule time
iris off 0.887 6.5 1.34 0.21 s
iris on 0.840 4.7 1.71 0.23 s
iris on-2x 0.840 4.7 1.65 0.41 s
wine off 0.807 7.8 1.90 0.26 s
wine on 0.923 7.0 2.14 0.29 s
wine on-2x 0.926 6.8 1.99 0.52 s
breast cancer off 0.926 7.6 1.83 0.54 s
breast cancer on 0.919 5.3 2.19 0.57 s
breast cancer on-2x 0.918 5.8 2.15 1.04 s
iris, mixture search off 0.919 11.2 1.59 1.5 s
iris, mixture search on 0.933 11.2 1.76 1.9 s
wine, mixture search off 0.908 10.4 1.50 1.9 s
wine, mixture search on 0.936 13.8 2.18 2.2 s
wine, mixture search on-2x 0.941 13.0 2.05 3.7 s
diabetes (R^2) off 0.235 20 2.31 0.52 s
diabetes (R^2) on 0.357 20 2.41 0.54 s
diabetes (R^2) on-2x 0.366 20 2.38 1.06 s

Standard deviations over the 10 runs are 0.03-0.09 (0.11-0.14 for R^2), so only wine and diabetes are clear gains and iris is within noise; the fits are small and tell nothing about the method in general. Rules with an at least/at most condition were 65-83% of the kept rules. Doubling the generations changed little here; with larger partitions or many features the larger space may need more.

Tests

tests/test_ordinal_conditions.py (58): alphabet and codes, membership (strong / Gaussian / interval Type-2 / product t-norm), the golden replay, array = object oracle (fixed and searched, T1 and T2, with and without batch_size), population and PyTorch routes, bounds / SBX / mutation, encode-decode round trip, duplicates, the three estimators, categorical and refused combinations, printing, rule_conditions, text / variable / pickle round trips, pattern-stability keys, reports and charts, latex, p-values. Run single files (the suite is large); ran everything file by file.

Not covered / limitations

FuzzyRulesClassifier, RuleMineClassifier, RuleFineTuneClassifier, FERL, and the random-rule null distribution of the bootstrap p-values (single terms) do not take the option. Refitting the same estimator after toggling the option off keeps the previous fit's flagged lvs (the library already retains partitions on refit); use clone.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qtw1LhwWgNJnXyJzGsLR2p

A rule can now state `x IS at least t` or `x IS at most t` over the ordered
terms of a variable, besides a single term. A variable with n terms offers
3n - 4 conditions (the n terms, and at least / at most t_j for j = 1..n-2);
the membership is the bounded sum min(1, sum of the covered terms), a
shouldered trapezoid on a strong partition.

The term gene is widened to the condition codes (no gene is added), so
sampling, crossover, mutation, the array / population / streamed / PyTorch
objectives and duplicate-rule detection handle it. Estimators:
BaseFuzzyRulesClassifier (winning rule, mixture, mixture_search) and
BaseFuzzyRulesRegressor (crisp and fuzzy). Printing, rule_conditions()
(operator / term without parsing), text persistence, pickles, pattern
stability and the latex / chart helpers handle it. Off by default: encoding,
rules, files and seeded fits equal 3.4.0 (replayed from a recorded fixture).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qtw1LhwWgNJnXyJzGsLR2p
@codecov-commenter

codecov-commenter commented Nov 10, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.72131% with 14 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.93%. Comparing base (10d811b) to head (7a0844a).

Files with missing lines Patch % Lines
ex_fuzzy/fuzzy_sets.py 96.60% 9 Missing ⚠️
ex_fuzzy/_torch_fitness.py 0.00% 5 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #45      +/-   ##
==========================================
+ Coverage   91.70%   91.93%   +0.23%     
==========================================
  Files          36       36              
  Lines        9146     9533     +387     
==========================================
+ Hits         8387     8764     +377     
- Misses        759      769      +10     
Flag Coverage Δ
unittests 91.93% <96.72%> (+0.23%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ordinal_conditions now also takes a collection of features (names or
indexes): only those variables get at least / at most, the rest keep single
terms (a cyclic clock variable, for example). The new compound_terms
parameter, {feature: [(name, [terms])...]}, adds one condition `x IS <name>`
per entry whose membership is the bounded sum of the listed terms; the terms
need not be contiguous (a union that wraps around is two shoulders).

Entries are validated (>= 2 distinct terms, not all, no repeated set, no
duplicate of an ordinal condition, a name unique within its variable) and go
through the same codes, compute_memberships rows, gather table, remap and
evaluators. They print by name, read as Condition(operator='any_of', terms,
name), and round-trip through the rule and variable text and pickle. Same
estimators; refused with candidate_rules. Defaults unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qtw1LhwWgNJnXyJzGsLR2p
@Fuminides
Fuminides merged commit ff35fbb into main Nov 10, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants