{"id":17204407,"url":"https://github.com/urschrei/cvmcount","last_synced_at":"2026-06-05T11:31:38.600Z","repository":{"id":241190749,"uuid":"804602525","full_name":"urschrei/cvmcount","owner":"urschrei","description":"A Rust implementation of the CVM algorithm for counting distinct elements in a stream","archived":false,"fork":false,"pushed_at":"2025-07-26T21:18:05.000Z","size":411,"stargazers_count":1,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-07-27T01:15:07.797Z","etag":null,"topics":["cardinality-estimation","count-distinct","distinct-elements"],"latest_commit_sha":null,"homepage":"https://crates.io/cvmcount","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/urschrei.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-05-22T22:51:42.000Z","updated_at":"2025-07-26T21:18:08.000Z","dependencies_parsed_at":"2025-06-29T22:26:18.584Z","dependency_job_id":"e455d16b-cbea-4547-b51f-b7fce3f42003","html_url":"https://github.com/urschrei/cvmcount","commit_stats":null,"previous_names":["urschrei/cvmcount"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/urschrei/cvmcount","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urschrei%2Fcvmcount","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urschrei%2Fcvmcount/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urschrei%2Fcvmcount/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urschrei%2Fcvmcount/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/urschrei","download_url":"https://codeload.github.com/urschrei/cvmcount/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/urschrei%2Fcvmcount/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33939225,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cardinality-estimation","count-distinct","distinct-elements"],"created_at":"2024-10-15T02:21:47.937Z","updated_at":"2026-06-05T11:31:38.594Z","avatar_url":"https://github.com/urschrei.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# A Rust implementation of the CVM Algorithm for Counting Distinct Elements\n\nThis library implements the algorithm described in\n\n\u003e Chakraborty, S., Vinodchandran, N. V., \u0026 Meel, K. S. (2022). *Distinct Elements in Streams: An Algorithm for the (Text) Book*. 6 pages, 727571 bytes. https://doi.org/10.4230/LIPIcs.ESA.2022.34\n\nThe accompanying article in Quanta is here: https://www.quantamagazine.org/computer-scientists-invent-an-efficient-new-way-to-count-20240516/\n\n## What does that mean\nThe count-distinct problem, or cardinality-estimation problem refers to counting the number of distinct elements in a data stream with repeated elements. As a concrete example, imagine that you want to count the unique words in a book. If you have enough memory, you can keep track of every unique element you encounter. However, you may not have enough working memory due to resource constraints, or the number of potential elements may be enormous. This constraint is referred to as the bounded-storage constraint in the literature.\n\nIn order to overcome this constraint, streaming algorithms have been developed: [Flajolet-Martin](https://en.wikipedia.org/wiki/Flajolet–Martin_algorithm), LogLog, [HyperLogLog](https://en.wikipedia.org/wiki/HyperLogLog). The algorithm implemented by this library is an improvement on these in one particular sense: it is extremely simple. Instead of hashing, it uses a sampling method to compute an [unbiased estimate](https://www.statlect.com/glossary/unbiased-estimator#:~:text=An%20estimator%20of%20a%20given,Examples) of the cardinality.\n\n# What is an Element\nIn this implementation, an element is anything implementing the `Ord` trait: various integer flavours, strings, any Struct on which you have implemented the trait. Not `f32` / `f64`, however (unless wrapped in an ordered wrapper type).\n\n## Ownership\nThe buffer has to keep ownership of its elements. In practice, this is not a problem: relative to its input stream size, the buffer is very small. This is also the point of the algorithm: your data set is very large and your working memory is small; you **don't** want to keep the original data around in order to store references to it! Thus, if you have `\u0026str` elements you will need to create new `String`s to store them. If you're processing text data you'll probably want to strip punctuation and regularise the case, so you'll need new `String`s anyway. If you're processing strings containing numeric values, parsing them to the appropriate integer type (which implements `Copy`) first seems like a reasonable approach.\n\n## Further Details\nDon Knuth has written about the algorithm (he refers to it as **Algorithm D**) at https://cs.stanford.edu/~knuth/papers/cvm-note.pdf, and does a far better job than I do at explaining it. You will note that on p1 he describes the buffer he uses as a data structure – called a [treap](https://en.wikipedia.org/wiki/Treap#:~:text=7%20External%20links-,Description,(randomly%20chosen)%20numeric%20priority.) – as a binary tree\n\u003e \"that’s capable of holding up to _s_ ordered pairs (_a_, _u_), where _a_ is an element of the stream and _u_ is a real number, 0 ≤ _u_ \u003c 1.\"\n\nwhere _s_ \u003e= 1. This implementation uses a treap as a buffer, following Knuth's original design. While this results in O(log n) operations instead of O(1) for hash-based approaches, it provides better cache locality for small buffers and eliminates hash collision overhead.\n\n# What does this library provide\nTwo things: the crate / library, and a command-line utility (`cvmcount`) which will count the unique strings in an input text file.\n\n# Installation\nBinaries and installation instructions are available for x64 Linux, Apple Silicon and Intel, and x64 Windows in [releases](https://github.com/urschrei/cvmcount/releases)\n\nYou can also build your own if you have Rust installed: `cargo install cvmcount`.\n\n# CLI Example\n\n```shell\ncvmcount -t file.txt -e 0.8 -d 0.1 -s 5000\n```\n`-t --tokens`: a valid path to a text file\n\n`-e --epsilon`: how close you want your estimate to be to the true number of distinct tokens. A smaller ε means you require a more precise estimate. For example, ε = 0.05 means you want your estimate to be within 5 % of the actual value. An epsilon of 0.8 is a good starting point for most applications.\n\n`-d --delta`: the level of certainty that the algorithm's estimate will fall within your desired accuracy range. A higher confidence (e.g. 99.9 %) means you're very sure the estimate will be accurate, while a lower confidence (e.g. 90 %) means there's a higher chance the estimate may be outside your desired range. A delta of 0.1 is a good starting point for most applications.\n\n`-s --streamsize`: this is used to determine buffer size and can be a loose approximation. The closer it is to the stream size, the more accurate the results.\n\nThe `--help` option is available.\n\n# Library Usage\n\nThe library provides both a simple constructor and a builder pattern for more ergonomic usage:\n\n## Simple Constructor\n\n```rust\nuse cvmcount::CVM;\n\nlet mut cvm = CVM::new(0.05, 0.01, 10_000);\nfor item in data_stream {\n    cvm.process_element(item);\n}\nlet estimate = cvm.calculate_final_result();\n```\n\n## Builder Pattern (Recommended)\n\nThe builder pattern provides better readability and validation:\n\n```rust\nuse cvmcount::CVM;\n\n// Using defaults (epsilon=0.8, confidence=0.9, size=1000)\nlet mut cvm: CVM\u003cString\u003e = CVM::builder().build().unwrap();\n\n// Custom configuration with confidence level\nlet mut cvm: CVM\u003ci32\u003e = CVM::builder()\n    .epsilon(0.05)        // 5 % accuracy\n    .confidence(0.99)     // 99 % confidence\n    .estimated_size(50_000)\n    .build()\n    .unwrap();\n\n// Using delta (failure probability) instead of confidence\nlet mut cvm: CVM\u003cString\u003e = CVM::builder()\n    .epsilon(0.1)         // 10 % accuracy\n    .delta(0.01)          // 1 % chance of failure\n    .estimated_size(1_000)\n    .build()\n    .unwrap();\n\n// Process your data\nfor word in text.split_whitespace() {\n    cvm.process_element(word.to_string());\n}\n\nlet estimate = cvm.calculate_final_result();\nprintln!(\"Estimated unique words: {}\", estimate as usize);\n```\n\nThe builder validates parameters and provides clear error messages for invalid inputs.\n\n## Streaming Interface\n\nFor processing iterators directly, you can use the streaming methods:\n\n```rust\nuse cvmcount::{CVM, EstimateDistinct};\n\n// Process an entire iterator with CVM instance\nlet mut cvm: CVM\u003ci32\u003e = CVM::builder().epsilon(0.05).build().unwrap();\nlet numbers = vec![1, 2, 3, 2, 1, 4, 5];\nlet estimate = cvm.process_stream(numbers);\n\n// Or use the iterator extension trait for one-liners\nlet estimate = (1..=1000)\n    .cycle()\n    .take(10_000)\n    .estimate_distinct_count(0.1, 0.1, 10_000);\n\n// With builder pattern\nlet words = vec![\"hello\".to_string(), \"world\".to_string(), \"hello\".to_string()];\nlet builder = CVM::\u003cString\u003e::builder().epsilon(0.05).confidence(0.99);\nlet estimate = words.into_iter().estimate_distinct_with_builder(builder).unwrap();\n\n// When working with borrowed data, map to owned explicitly\nlet borrowed_words = vec![\"hello\", \"world\", \"hello\"];\nlet estimate = borrowed_words\n    .iter()\n    .map(|s| s.to_string())\n    .estimate_distinct_count(0.1, 0.1, 1000);\n```\n\nThe streaming interface accepts owned values to avoid cloning within the algorithm, making the ownership requirements explicit.\n\n## Analysis\n\n![](cvmcount.png)\n```text\nMean: 9015.744000\nStd: 534.076058\nMin 7552.000000\n25% 8672.000000\n50% 9024.000000\n75% 9344.000000\nMax 11072.00000\n```\n\n## Note\nIf you're thinking about using this library, you presumably know that it only provides an estimate (within the specified bounds), similar to something like HyperLogLog. You are trading accuracy for speed and memory usage!\n\n## Perf\nCalculating the unique tokens in a [418K UTF-8 text file](https://www.gutenberg.org/ebooks/8492) using the CLI takes 7.2 ms ± 0.3 ms on an M2 Pro. Counting 10e6 7-digit integers takes around 13.5 ms. An exact count using the same regex and HashSet runs in around 18 ms. Run `cargo bench` for more.\n\n## Implementation Details\nThe CLI app strips punctuation from input tokens using a regex. I assume there is a small performance penalty, but it seems like a small price to pay for increased practicality.\n\n \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Furschrei%2Fcvmcount","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Furschrei%2Fcvmcount","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Furschrei%2Fcvmcount/lists"}