Skip to content

Commit

Permalink
Add CLI (#35)
Browse files Browse the repository at this point in the history
* Initial CLI implementation

* Fix CLI quirks

* Add `smoothing` parameter I forgot

* Improve README.md

* Fix linter and tox
  • Loading branch information
InCogNiTo124 authored Apr 21, 2022
1 parent a0ed53b commit a9dbd5d
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 8 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Detect knee points in various scenarios using a plethora of methods


## Usage
Just plugin your values in a `list`, `tuple` or an `np.ndarray` and watch `knarrow` hit the knee:
Just plug in your values in a `list`, `tuple` or an `np.ndarray` and watch `knarrow` hit the knee:

```pycon
>>> from knarrow import find_knee
Expand Down Expand Up @@ -54,7 +54,18 @@ array([[0. , 1. ],
4
```

### Similar projects
### CLI
This library also comes with a handy CLI:
```shell
$ cat data.txt | knarrow
stdin 11
$ cat data.txt | knarrow -o value
stdin 59874.14171519781845532648
$ knarrow --sort -d ',' -o value shuf_delim.txt
shuf_delim.txt 20
```

## Similar projects

While I've come up with most of these methods by myself, I am not the only one. Here is a (non-comprehensive) list of projects I've found that implement a similar funcionality and may have been an inspiration for me:
- [mariolpantunes/knee](https://github.com/mariolpantunes/knee)
Expand Down
8 changes: 8 additions & 0 deletions docs/api/apidoc/knarrow.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
knarrow package
===============

Subpackages
-----------

.. toctree::
:maxdepth: 4

knarrow.cli

Submodules
----------

Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@
"Topic :: Utilities",
"Typing :: Typed",
],
entry_points={"console_scripts": ["knarrow=knarrow.cli.__main__:main"]},
)
Empty file added src/knarrow/cli/__init__.py
Empty file.
88 changes: 88 additions & 0 deletions src/knarrow/cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from collections import Counter
from functools import partial
from pathlib import Path

from knarrow import find_knee


def gte_0(value):
x = float(value)
assert x >= 0.0
return x


METHODS = [
"angle",
"c_method",
"distance",
"distance_adjacent",
"kneedle",
"menger_anchored",
"menger_successive",
"ols_swiping",
]


def get_parser():
parser = ArgumentParser(prog="knarrow", formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument(
"-m", "--method", choices=(["all"] + METHODS), default="all", help="select the knee searching method"
)
parser.add_argument(
"--sort",
action="store_true",
help="sort the values before the knee search. By default is assumes the input is already sorted",
)
parser.add_argument("--smoothing", default=0.0, type=gte_0, help="cublic spline smoothing parameter")
parser.add_argument(
"-d", "--delimiter", default=None, help="split the values with DELIMITER. If None, split by space"
)
parser.add_argument(
"-o",
"--output",
choices=["index", "value"],
default="index",
help=(
"if output is `value`, this will return the row of the input file where the knee was detected. "
"if output is `index`, the index of that row will be returned"
),
)
parser.add_argument("files", nargs="*", default=["-"], help="a list of files. STDIN is denoted with `-`.")
return parser


def cli(method="all", files=None, sort=False, delimiter=None, output=None, smoothing=None):
for filename in files:
path = Path("/dev/stdin" if filename == "-" else filename)
with path.open("r") as file:
rows = list(map(str.strip, file))
split = partial(str.split, sep=delimiter)
values = map(split, rows)
numbers = list(tuple(float(value) for value in row) for row in values)
indices = list(range(len(numbers)))
if sort:
indices.sort(key=lambda i: numbers[i])
key_function = (lambda x: x) if len(numbers[0]) == 1 else (lambda x: x[0])
numbers.sort(key=key_function)

if method == "all":
counter = Counter([find_knee(numbers, method=m, sort=False, smoothing=smoothing) for m in METHODS])
most_common = counter.most_common(1).pop(0)
knee = most_common[0]
else:
knee = find_knee(numbers, method=method, sort=False, smoothing=smoothing)

result = indices[knee] if output == "index" else rows[indices[knee]]
print(path.name, result)
return


def main():
parser = get_parser()
args = vars(parser.parse_args())
exit(cli(**args))


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion src/knarrow/ols.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def r_squared(x, y):
sst = np.var(y, ddof=n - 1)
r2_1 = (1 - ssr / sst).item()
corr = np.corrcoef(w @ xx, y)[0, 1]
r2_2 = corr ** 2
r2_2 = corr**2
assert np.allclose(r2_1, r2_2)
return ((r2_1 + r2_2) / 2).item()

Expand Down
4 changes: 3 additions & 1 deletion src/knarrow/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def inner(*args, **kwargs):

# more or less all the algorithms depend on the inputs to be sorted, at least in the x dimension
# therefore the x and y are sorted together
if not np.all(np.diff(x) > 0):
# the user should explicitly disallow sorting
perform_sort = kwargs.pop("sort", True)
if perform_sort and not np.all(np.diff(x) > 0):
sorted_indices = np.argsort(x) # sort in the ascending way
x = x[sorted_indices]
y = y[sorted_indices]
Expand Down
15 changes: 15 additions & 0 deletions tests/cli/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
1.00000000000000000000
2.71828182845904523536
7.38905609893065022723
20.08553692318766774092
54.59815003314423907811
148.41315910257660342111
403.42879349273512260838
1096.63315842845859926372
2980.95798704172827474359
8103.08392757538400770999
22026.46579480671651695790
59874.14171519781845532648
162754.79141900392080800520
442413.39200892050332610277
1202604.28416477677774923677
21 changes: 21 additions & 0 deletions tests/cli/shuf_delim.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
18,.85447998506288733923
17,.91167918637384475033
4,5.20650605348167213512
13,1.24537072699740781656
10,1.70665642170428796832
20,.75957113438621465853
2,11.10336757201109052117
1,20.08312627154631767497
12,1.36981506456443459268
7,2.64233344933772314492
14,1.14119975942722448362
9,1.94063983061649768141
16,.97719576435870593359
8,2.24217221922731762267
5,3.98762418529180844687
11,1.52066319213249374626
15,1.05289037728159810697
19,.80415538149886314748
6,3.19315561035895693094
0,51.19436311832002749043
3,7.24325504903906741104
6 changes: 6 additions & 0 deletions tests/cli/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
1
2
3
6
9
12
8 changes: 4 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ skip_install = true
deps =
-r{toxinidir}/dev-requirements.txt
commands =
isort .
black .
isort src/knarrow
black src/knarrow

[testenv:fmt-check]
skip_install = true
deps =
-r{toxinidir}/dev-requirements.txt
commands =
isort --check .
black --check .
isort --check src/knarrow
black --check src/knarrow

[testenv:docs-api]
skip_install = true
Expand Down

0 comments on commit a9dbd5d

Please sign in to comment.