{"id":17384558,"url":"https://github.com/pgdr/samplitude","last_synced_at":"2025-07-29T04:05:24.066Z","repository":{"id":57463775,"uuid":"141269185","full_name":"pgdr/samplitude","owner":"pgdr","description":"CLI generation and plotting of random variables","archived":false,"fork":false,"pushed_at":"2020-11-18T10:31:54.000Z","size":236,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-04-09T03:04:44.948Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pgdr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-07-17T09:50:46.000Z","updated_at":"2020-11-18T10:31:57.000Z","dependencies_parsed_at":"2022-09-17T14:20:14.705Z","dependency_job_id":null,"html_url":"https://github.com/pgdr/samplitude","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pgdr/samplitude","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fsamplitude","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fsamplitude/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fsamplitude/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fsamplitude/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pgdr","download_url":"https://codeload.github.com/pgdr/samplitude/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pgdr%2Fsamplitude/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263957820,"owners_count":23535604,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":[],"created_at":"2024-10-16T07:45:51.830Z","updated_at":"2025-07-06T19:08:05.706Z","avatar_url":"https://github.com/pgdr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Samplitude [![Build Status](https://travis-ci.com/pgdr/samplitude.svg?branch=master)](https://travis-ci.com/pgdr/samplitude)\n\nCLI generation and plotting of random variables:\n\n```bash\n$ samplitude \"sin(0.31415) | sample(6) | round | cli\"\n0.0\n0.309\n0.588\n0.809\n0.951\n1.0\n```\n\nThe word _samplitude_ is a portmanteau of _sample_ and _amplitude_.  This\nproject also started as an étude, hence should be pronounced _sampl-étude_.\n\n`samplitude` is a chain starting with a _generator_, followed by zero or more\n_filters_, followed by a consumer.  Most generators are infinite (with the\nexception of `range` and `lists` and possibly `stdin`).  Some of the filters can\nturn infinite generators into finite generators (like `sample` and `gobble`),\nand some filters can turn finite generators into infinite generators, such as\n`choice`.\n\n_Consumers_ are filters that necessarily flush the input; `list`, `cli`,\n`json`, `unique`, and the plotting tools, `hist`, `scatter` and `line` are\nexamples of consumers.  The `list` consumer is a Jinja2 built-in, and other\nJinja2 consumers are `sum`, `min`, and `max`:\n\n```bash\nsamplitude \"sin(0.31415) | sample(5) | round | max | cli\"\n0.951\n```\n\nFor simplicity, **s8e** is an alias for samplitude.\n\n\n##  Generators\n\nIn addition to the standard `range` function, we support infinite generators\n\n* `exponential(lambd)`: `lambd` is 1.0 divided by the desired mean.\n* `uniform(a, b)`: Get a random number in the range `[a, b)` or `[a, b]`\n  depending on rounding.\n* `gauss(mu, sigma)`: `mu` is the mean, and `sigma` is the standard deviation.\n* `normal(mu, sigma)`: as above\n* `lognormal(mu, sigma)`: as above\n* `triangular(low, high)`: Continuous distribution bounded by given lower and\n  upper limits, and having a given mode value in-between.\n* `beta(alpha, beta)`: Conditions on the parameters are `alpha \u003e 0` and `beta \u003e\n  0`.  Returned values range between 0 and 1.\n* `gamma(alpha, beta)`: as above\n* `weibull(alpha, beta)`: `alpha` is the scale parameter and `beta` is the shape\n  parameter.\n* `pareto(alpha)`: Pareto distribution.  `alpha` is the shape parameter.\n* `vonmises(mu, kappa)`: `mu` is the mean angle, expressed in radians between 0\n  and `2*pi`, and `kappa` is the concentration parameter, which must be greater\n  than or equal to zero.  If kappa is equal to zero, this distribution reduces\n  to a uniform random angle over the range 0 to `2*pi`.\n\nProvided that you have installed the `scipy.stats` package, the\n* `pert(low, peak, high)`\ndistribution is supported.\n\nWe have a special infinite generator (filter) that works on finite generators:\n\n* `choice`,\n\nwhose behaviour is explained below.\n\nFor input from files, either use `words` with a specified environment variable\n`DICTIONARY`, or pipe through\n\n* `stdin()`\n\nwhich reads from `stdin`.\n\nIf the file is a csv file, there is a `csv` generator that reads a csv file with\nPandas and outputs the first column (if nothing else is specified).  Specify the\ncolumn with either an integer index or a column name:\n\n```bash\n\u003e\u003e\u003e samplitude \"csv('iris.csv', 'virginica') | counter | cli\"\n0 50\n1 50\n2 50\n```\n\nFor other files, we have the `file` generator:\n```bash\n\u003e\u003e\u003e s8e \"file('iris.csv') | sample(1) | cli\"\n150,4,setosa,versicolor,virginica\n```\n\n\nFinally, we have `combinations` and `permutations` that are inherited from\nitertools and behave exactly like those.\n\n```bash\n\u003e\u003e\u003e s8e \"'ABC' | permutations | cli\"\n```\n\nHowever, the output of this is rather non-UNIXy, with the abstractions leaking through:\n```bash\n\u003e\u003e\u003e s8e \"'HT' | permutations | cli\"\n('H', 'T')\n('T', 'H')\n```\n\nSo to get a better output, we can use an _elementwise join_ `elt_join`:\n```bash\n\u003e\u003e\u003e s8e \"'HT' | permutations | elt_join | cli\"\nH T\nT H\n```\n\nwhich also takes a seperator as argument:\n```bash\n\u003e\u003e\u003e s8e \"'HT' | permutations | elt_join(';') | cli\"\nH;T\nT;H\n```\n\nThis is already supported by Jinja's `map` function (notice the strings around `join`):\n```bash\n\u003e\u003e\u003e s8e \"'HT' | permutations | map('join', ';') | cli\"\nH;T\nT;H\n```\n\nWe can thus count the number of permutations of a set of size 10:\n```bash\n\u003e\u003e\u003e s8e \"range(10) | permutations | len\"\n3628800\n```\n\n\nThe `product` generator takes two generators and computes a cross-product of\nthese.  In addition,\n\n## A warning about infinity\n\nAll generators are (potentially) infinite generators, and must be sampled with\n`sample(n)` before consuming!\n\n## Usage and installation\n\nInstall with\n```bash\npip install samplitude\n```\nor to get bleeding release,\n```bash\npip install git+https://github.com/pgdr/samplitude\n```\n\n\n### Examples\n\nThis is pure Jinja2:\n```bash\n\u003e\u003e\u003e samplitude \"range(5) | list\"\n[0, 1, 2, 3, 4]\n```\n\nHowever, to get a more UNIXy output, we use `cli` instead of `list`:\n\n```bash\n\u003e\u003e\u003e s8e \"range(5) | cli\"\n0\n1\n2\n3\n4\n```\n\nTo limit the output, we use `sample(n)`:\n\n\n```bash\n\u003e\u003e\u003e s8e \"range(1000) | sample(5) | cli\"\n0\n1\n2\n3\n4\n```\n\nThat isn't very helpful on the `range` generator, which is already finite, but\nis much more helpful on an infinite generator.  The above example is probably\nbetter written as\n\n```bash\n\u003e\u003e\u003e s8e \"count() | sample(5) | cli\"\n0\n1\n2\n3\n4\n```\n\nHowever, much more interesting are the infinite random generators, such as the\n`uniform` generator:\n\n```bash\n\u003e\u003e\u003e s8e \"uniform(0, 5) | sample(5) | cli\"\n3.3900198868059235\n1.2002767137709318\n0.40999391897569126\n1.9394585953696264\n4.37327472704115\n```\n\nWe can round the output in case we don't need as many digits (note that `round`\nis a generator as well and can be placed on either side of `sample`):\n```bash\n\u003e\u003e\u003e s8e \"uniform(0, 5) | round(2) | sample(5) | cli\"\n4.98\n4.42\n2.05\n2.29\n3.34\n```\n\n\n\n### Selection and modifications\n\nThe `sample` behavior is equivalent to the `head` program, or from languages\nsuch as Haskell. The `head` alias is supported:\n```bash\n\u003e\u003e\u003e samplitude \"uniform(0, 5) | round(2) | head(5) | cli\"\n4.58\n4.33\n1.87\n2.09\n4.8\n```\n\n`drop` is also available:\n```bash\n\u003e\u003e\u003e s8e \"uniform(0, 5) | round(2) | drop(2) | head(3) | cli\"\n1.87\n2.09\n4.8\n```\n\nTo **shift** and **scale** distributions, we can use the `shift(s)` and\n`scale(s)` filters.\nTo get a Poisson distribution process starting at 15, we can run\n\n```bash\n\u003e\u003e\u003e s8e \"poisson(4) | shift(15) | sample(5) |cli\"\n18\n21\n19\n22\n17\n```\n\nor to get the Poisson point process (exponential distribution),\n\n```bash\n\u003e\u003e\u003e s8e \"exponential(4) | round | shift(15) | sample(5) |cli\"\n16.405\n15.54\n15.132\n15.153\n15.275\n```\n\n\n\nBoth `shift` and `scale` work on generators, so to add `sin(0.1)` and\n`sin(0.2)`, we can run\n```bash\n\u003e\u003e\u003e s8e \"sin(0.1) | shift(sin(0.2)) | sample(10) | cli\"\n```\n\n![sin(0.1)+sin(0.2) line](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/line_sin01sin02.png)\n\n\n\n### Choices and other operations\n\nUsing `choice` with a finite generator gives an infinite generator that chooses\nfrom the provided generator:\n\n```bash\n\u003e\u003e\u003e samplitude \"range(0, 11, 2) | choice | sample(6) | cli\"\n8\n0\n8\n10\n4\n6\n```\n\nJinja2 supports more generic lists, e.g., lists of strings.  Hence, we can write\n\n```bash\n\u003e\u003e\u003e s8e \"['win', 'draw', 'loss'] | choice | sample(6) | sort | cli\"\ndraw\ndraw\nloss\nloss\nloss\nwin\n```\n\n... and as in Python, strings are also iterable:\n\n```bash\n\u003e\u003e\u003e s8e \"'HT' | cli\"\nH\nT\n```\n... so we can flip six coins with\n```bash\n\u003e\u003e\u003e s8e \"'HT' | choice | sample(6) | cli\"\nH\nT\nT\nH\nH\nH\n```\n\nWe can flip 100 coins and count the output with `counter` (which is\n`collections.Counter`)\n```bash\n\u003e\u003e\u003e s8e \"'HT' | choice | sample(100) | counter | cli\"\nH 47\nT 53\n```\n\nThe `sort` functionality works as expected on a `Counter` object (a\n`dict` type), so if we want the output sorted by key, we can run\n\n```bash\n\u003e\u003e\u003e s8e \"range(1,7) | choice | sample(100) | counter | sort | elt_join | cli\" 42 # seed=42\n1 17\n2 21\n3 12\n4 21\n5 13\n6 16\n```\n\nThere is a minor hack to sort by value, namely by `swap`-ing the Counter twice:\n```bash\n\u003e\u003e\u003e s8e \"range(1,7) | choice | sample(100) |\n         counter | swap | sort | swap | elt_join | cli\" 42 # seed=42\n3 12\n5 13\n6 16\n1 17\n2 21\n4 21\n```\n\nThe `swap` filter does an element-wise reverse, with element-wise reverse\ndefined on a dictionary as a list of `(value, key)` for each key-value pair in\nthe dictionary.\n\nSo, to get the three most common anagram strings, we can run\n```bash\n\u003e\u003e\u003e s8e \"words() | map('sort') | counter | swap | sort(reverse=True) |\n         swap | sample(3) | map('first') | elt_join('') | cli\"\naeprs\nacerst\nopst\n```\n\n\nUsing `stdin()` as a generator, we can pipe into `samplitude`.  Beware that\n`stdin()` flushes the input, hence `stdin` (currently) does not work with\ninfinite input streams.\n\n```bash\n\u003e\u003e\u003e ls | samplitude \"stdin() | choice | sample(1) | cli\"\nsome_file\n```\n\n\nThen, if we ever wanted to shuffle `ls` we can run\n\n```bash\n\u003e\u003e\u003e ls | samplitude \"stdin() | shuffle | cli\"\nsome_file\n```\n\n```bash\n\u003e\u003e\u003e cat FILE | samplitude \"stdin() | cli\"\n# NOOP; cats FILE\n```\n\n\n\n### The fun powder plot\n\nFor fun, if you have installed `matplotlib`, we support plotting, `hist` being\nthe most useful.\n\n```bash\n\u003e\u003e\u003e samplitude \"normal(100, 5) | sample(1000) | hist\"\n```\n\n![normal distribution](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/hist_normal.png)\n\nAn exponential distribution can be plotted with `exponential(lamba)`.  Note that\nthe `cli` output must be the last filter in the chain, as that is a command-line\nutility only:\n\n```bash\n\u003e\u003e\u003e s8e \"normal(100, 5) | sample(1000) | hist | cli\"\n```\n\n![exponential distribution](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/hist_exponential.png)\n\n\nTo **repress output after plotting**, you can use the `gobble` filter to empty\nthe pipe:\n\n```bash\n\u003e\u003e\u003e s8e \"normal(100, 5) | sample(1000) | hist | gobble\"\n```\n\n\nThe\n[`pert` distribution](https://en.wikipedia.org/wiki/PERT_distribution)\ntakes inputs `low`, `peak`, and `high`:\n\n```bash\n\u003e\u003e\u003e s8e \"pert(10, 50, 90) | sample(100000) | hist(100) | gobble\"\n```\n\n![PERT distribution](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/hist_pert.png)\n\n\n\nAlthough `hist` is the most useful, one could imaging running `s8e` on\ntimeseries, where a `line` plot makes most sense:\n\n```bash\n\u003e\u003e\u003e s8e \"sin(22/700) | sample(200) | line\"\n```\n\n![sine and line](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/line_sine.png)\n\n\nThe scatter function can also be used, but requires that the input stream is a\nstream of pairs, which can be obtained either by the `product` generator, or via\nthe `pair` or `counter` filter:\n\n```bash\ns8e \"normal(100, 10) | sample(10**5) | round(0) | counter | scatter\"\n```\n\n![scatter normal](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/scatter_normal_counter.png)\n\n\n\n### Fourier\n\nA fourier transform is offered as a filter `fft`:\n\n\n```bash\n\u003e\u003e\u003e samplitude \"sin(0.1) | shift(sin(0.2)) | sample(1000) | fft | line | gobble\"\n```\n\n![fft line](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/line_fft.png)\n\n\n## Your own filter\n\nIf you use Samplitude programmatically, you can register your own filter by\nsending a dictionary\n\n```python\n{'name1' : filter1,\n 'name2' : filter2,\n #...,\n 'namen' : filtern,\n}\n```\nto the `samplitude` function.\n\n### Example: secretary problem\nSuppose you want to emulate the secretary problem ...\n\n#### Intermezzo: The problem\nFor those not familiar, you are a boss, Alice, who wants to hire a new secretary\nBob.  Suppose you want to hire the tallest Bob of all your candidates, but the\ncandidates arrive in a stream, and you know only the number of candidates.  For\neach candidate, you have to accept (hire) or reject the candidate.  Once you\nhave rejected a candidate, you cannot undo the decision.\n\nThe solution to this problem is to look at the first `n/e` (`e~2.71828` being\nthe Euler constant) candidates, and thereafter accept the first candidate taller\nthan all of the `n/e` first candidates.\n\n#### A Samplitude solution\n\nLet `normal(170, 10)` be the candidate generator, and let `n=100`.  We create a\nfilter `secretary` that takes a stream and an integer (`n`) and picks according\nto the solution.  In order to be able to assess the quality of the solution\nlater, the filter must forward the entire list of candidates; hence we annotate\nthe one we choose with `(c, False)` for a candidate we rejected, and `(c, True)`\ndenotes the candidate we accepted.\n\n```python\ndef secretary(gen, n):\n    import math\n    explore = int(n / math.e)\n    target = -float('inf')\n    i = 0\n\n    # explore the first n/e candidates\n    for c in gen:\n        target = max(c, target)\n        yield (c, False)\n        i += 1\n        if i == explore:\n            break\n\n    _ok = lambda c, i, found: ((i == n-1 and not found)\n                            or (c \u003e target and not found))\n\n    have_hired = False\n    for c in gen:\n        status = _ok(c, i, have_hired)\n        have_hired = have_hired or status\n        yield c, status\n        i += 1\n        if i == n:\n            return\n```\n\nNow, to emulate the secretary problem with Samplitude:\n\n```python\nfrom samplitude import samplitude as s8e\n\n# insert above secretary function\n\nn = 100\nfilters = {'secretary': secretary}\n\nsolution = s8e('normal(170, 10) | secretary(%d) | list' % n, filters=filters)\nsolution = eval(solution)  # Samplitude returns an eval-able string\ncands = map(lambda x: x[0], solution)\nopt = [s[0] for s in solution if s[1]][0]\n# the next line prints in which position the candidate is\nprint(1+sorted(cands, reverse=True).index(opt), '/', n)\n```\n\nIn about 67% of the cases we can expect to get one of the top candidates,\nwhereas the remaining 33% of the cases will be uniformly distributed.  Running\n100k runs with a population of size 1000 reveals the structure.\n\n![Secretary selection](https://raw.githubusercontent.com/pgdr/samplitude/master/assets/hist_secretary.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdr%2Fsamplitude","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpgdr%2Fsamplitude","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpgdr%2Fsamplitude/lists"}