Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 KiB
Category suggestion in the expense editor
Proposal, 2026-10-18. The owner decided that the expense editor (S-7) fills in the category from the description while the user types, and that the user can always change it. This file is the proposed rule as spec text, the evidence from the household's Splitwise export behind it, and the open points. The built-in keyword table is category-keywords.csv next to this file. Nothing here is in SPEC.md yet; if accepted it becomes an addition to AC-8.6 with a decision entry.
Proposed rule (spec text)
CS-1 Scope. The suggestion runs only in the editor for a new expense. It never runs when editing an existing expense, and never changes the category of a saved expense.
CS-2 Manual choice wins. The editor keeps a flag "category picked by hand", false when the editor opens. Picking any category in the category grid sets it to true, also when the user picks the category that is already shown. While the flag is true, the suggestion never changes the category for the rest of this editor session (until the editor closes). Nothing resets the flag.
CS-3 When it runs. While the flag is false, the category is recomputed on every change of the description and on every change of the account. It is a local computation over Room data; no network.
CS-4 Normalisation. Descriptions (typed and stored) and keywords are compared after this normalisation:
- Unicode NFC, then lowercase without locale rules (Kotlin
lowercase(),Locale.ROOT). - Delete
',’and:(so "Willy:s" becomes "willys", "McDonald's" becomes "mcdonalds"). - Replace every remaining character that is not a Unicode letter, a Unicode digit, whitespace or
&with a space. - Collapse runs of whitespace to one space and trim.
Diacritics are kept (å, ä, ö and é stay distinct; the keyword table lists both spellings where it matters). Digits are kept. The tokens are the normalised description split on spaces; the first token is the first of them. An empty normalised description gives no history or keyword match.
CS-5 Order. The suggested category is the first that applies of:
- History, exact: among the non-deleted expenses on the phone (every account, either user, including imported and recurring ones) whose normalised description equals the normalised typed description, the category used most often among them; on a tie, the one used most recently.
- History, first token: among the non-deleted expenses whose first token equals the typed description's first token, the category used most often among them; on a tie, the one used most recently.
- Keywords: the category of the winning entry of the built-in keyword table (CS-6).
- Default: the AC-8.6 default (the category last used in the selected account on this phone if still active, else
other).
"Used most often" counts the matching expenses per category; "most recently" is the expense-list order of AC-8.10 (expense_date descending, then created_at descending), applied to the latest expense of each tied category (owner, 2026-10-18: most often, not most recent). Categories that are archived or deleted are left out of the count; when no category is left, the next step applies.
CS-6 Keyword table. Each entry has keyword (already normalised), match and default_key. An entry matches when:
token: some token equals the keyword;prefix: some token starts with the keyword;phrase: the keyword's tokens occur consecutively in the tokens.
When several entries match, the longest keyword (in code points, spaces included) wins; on equal length, the entry earlier in the table wins (the table is sorted by default_key, then keyword). The winning entry gives the category with that default_key (renamed defaults included, since the key stays). If that category is archived, the next matching entry in the same order is tried; when none is left, step 4 applies. The table is fixed data shipped with the app; users do not edit it.
CS-7 What the user sees. The suggestion simply replaces the category shown in the Kategori field: no caption, no marker, no confirmation (owner, 2026-10-18: a wrong guess is corrected with one tap). Clearing the description returns the field to the step-4 default.
Evidence
Source: the household's two Splitwise exports, read-only on the owner's Mac. 1 775 expense rows from 2020-11 to 2026-10 (payment rows, blank lines and the total row excluded), mapped to the 14 Kvitt categories with the owner's revised import mapping of 2026-10-18 (§10.8 plus Rent, Mortgage, Hyra, Avbetalning/Amortering → housing; Insurance, Försäkringar → insurance; Childcare, Barnomsorg → children; Taxes, Skatter → fees; and the nine Swedish names that were missing). No row is unmapped. True categories: groceries 662, restaurants 301, home 164, housing 154, entertainment 113, transport 90, children 73, utilities 61, other 39, travel 37, insurance 26, shopping 19, fees 19, health 17.
Method: chronological simulation. Rows are walked in date order; each row's category is predicted from earlier rows only and compared with its true category. The step-4 default is approximated as the previous row's category in the same file (one file is one account), other for the first row.
History variants alone (coverage = share of rows with a prediction; accuracy among those; "most often" breaks ties by most recent):
| Variant | Coverage | Most often | Most recent |
|---|---|---|---|
| Exact normalised description | 56.2 % | 95.6 % | 94.6 % |
| First two tokens | 57.2 % | 95.5 % | 94.5 % |
| First token | 71.5 % | 92.6 % | 91.4 % |
| Exact, then first token (chosen) | 71.5 % | 92.8 % | 91.9 % |
| Any shared token of ≥ 3 letters | 79.0 % | 89.0 % | 84.8 % |
| Any shared token of ≥ 4 letters | 63.7 % | 88.0 % | 83.9 % |
Exact-then-first-token keeps the owner's "matching description" literally as step 1 and adds the first token for coverage. "Most often" is about one point better than "most recent" in every variant. Dropping digit-only tokens changed nothing.
Full rule (CS-5, chosen variant, keyword table as committed):
| Stage | Share of rows | Accuracy |
|---|---|---|
| History, exact (998 rows) | 56.2 % | 95.6 % |
| History, first token (272 rows) | 15.3 % | 82.7 % |
| Keywords (145 rows) | 8.2 % | 92.4 % |
| Default, approximated (360 rows) | 20.3 % | 28.3 % |
| Overall | 100 % | 79.7 % |
For comparison: the AC-8.6 default alone 30.5 %; history and default without keywords 74.7 %; the full rule with "most recent" history 79.0 %.
Cold start (keywords and default only, no history): first 10 % of rows (177): keywords cover 77.4 % at 95.6 % accuracy, 83.6 % overall. Over all rows: 72.7 % coverage at 96.0 %. These are in-sample for the data entries. An out-of-sample check (keep a data entry only if it reaches ≥ 2 rows and ≥ 80 % precision in the first half; test on the second half) gives 57.9 % coverage at 97.5 % accuracy.
Most common wrong suggestions (suggested → true, full rule, 360 wrong of 1 775: 258 at the default step, 91 at history, 11 at keywords): groceries → restaurants 53, restaurants → groceries 30, groceries → home 24, groceries → other 15, groceries → children 14, transport → restaurants 9, housing → home 9, groceries → entertainment 8. History errors are mostly the groceries/restaurants pair on a shared first token such as "mat". Keyword errors are spread thin (apotek and mat 2 each, the rest 1).
Keyword table: 148 entries, 79 data and 69 curated; 114 token, 28 prefix, 6 phrase. No entry maps to other.
default_key |
data | curated |
|---|---|---|
| groceries | 20 | 4 |
| restaurants | 17 | 8 |
| home | 10 | 4 |
| housing | 5 | 2 |
| utilities | 1 | 7 |
| transport | 6 | 10 |
| travel | 4 | 3 |
| entertainment | 7 | 5 |
| health | 1 | 7 |
| shopping | 1 | 7 |
| insurance | 2 | 5 |
| children | 4 | 3 |
| fees | 1 | 4 |
data entries predicted one category with high precision in the export (most ≥ 90 %; ikea 79 %, the misses being the IKEA restaurant; apotek 55 %, kept because the meaning is unambiguous, the household filed pharmacy purchases under health, children, other and home). curated entries are well-known Swedish chains and services and unambiguous words, absent or rare in the export. Two curated entries go against the few rows in the export, by the owner's examples: barnförsäkring → insurance (3 rows filed under childcare) and blöjor → children (2 of 3 rows filed under groceries).
The five former other entries were moved: försäkring, hemförsäkring, barnförsäkring → insurance; ränteåterbäring → fees (17 of 18 rows were filed under taxes); förskola → children (now the prefix förskol). hyra, ränta, lgh, brf and amortering moved from home to housing, and avgift was added (2 of 2 rows housing).
Excluded on purpose
Kinds of word left out of the table even when they predicted well:
- person names and nicknames, addresses, street names, workplaces, private places and anything that could identify a person;
- town, city and country names (they predict a trip, not a category);
- names of single local restaurants, cafés and shops whose identity was unclear;
- function words and abbreviations (och, till, för, på, mm, etc, o, &);
- words the household used against their dictionary meaning:
el(always part of "hyra och el", filed under housing),kläder(filed under childcare in the export; generic clothes belong to shopping); and words split between categories:frukost,vin,öl,blommor,brunch,cykel,ansiktstvätt.
Open points
- Import mapping. The replay uses the owner's revised mapping (14 categories, nine added Swedish names); SPEC §10.8 and AC-7.2 still list ten categories and must be updated with it.
- Imported history. After M11 the whole Splitwise history is in Room, so for this household history decides most descriptions from day one; the keyword table matters for new words and after a fresh install.
- Recurring template editor. Not covered; the same rule could apply to new templates.
- Recency key. CS-5 breaks ties by list order (
expense_date). Usingupdated_atinstead would let a re-categorised old expense count as recent; not measurable from the export. - Where the table lives. Proposed:
:domainas data plus a unit test that loadsdocs/ux/category-keywords.csv(or its successor) and checks every keyword is already normalised, unique, and maps to an existingdefault_key. - English keywords. The table is Swedish plus brand names; English words are not covered.