summary.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. #!/usr/bin/env python3
  2. #
  3. # Script to summarize the outputs of other scripts. Operates on CSV files.
  4. #
  5. # Example:
  6. # ./scripts/code.py lfs.o lfs_util.o -q -o lfs.code.csv
  7. # ./scripts/data.py lfs.o lfs_util.o -q -o lfs.data.csv
  8. # ./scripts/summary.py lfs.code.csv lfs.data.csv -q -o lfs.csv
  9. # ./scripts/summary.py -Y lfs.csv -f code=code_size,data=data_size
  10. #
  11. # Copyright (c) 2022, The littlefs authors.
  12. # SPDX-License-Identifier: BSD-3-Clause
  13. #
  14. import collections as co
  15. import csv
  16. import functools as ft
  17. import glob
  18. import itertools as it
  19. import math as m
  20. import os
  21. import re
  22. CSV_PATHS = ['*.csv']
  23. # supported merge operations
  24. #
  25. # this is a terrible way to express these
  26. #
  27. OPS = {
  28. 'sum': lambda xs: sum(xs[1:], start=xs[0]),
  29. 'prod': lambda xs: m.prod(xs[1:], start=xs[0]),
  30. 'min': min,
  31. 'max': max,
  32. 'mean': lambda xs: FloatField(sum(float(x) for x in xs) / len(xs)),
  33. 'stddev': lambda xs: (
  34. lambda mean: FloatField(
  35. m.sqrt(sum((float(x) - mean)**2 for x in xs) / len(xs)))
  36. )(sum(float(x) for x in xs) / len(xs)),
  37. 'gmean': lambda xs: FloatField(m.prod(float(x) for x in xs)**(1/len(xs))),
  38. 'gstddev': lambda xs: (
  39. lambda gmean: FloatField(
  40. m.exp(m.sqrt(sum(m.log(float(x)/gmean)**2 for x in xs) / len(xs)))
  41. if gmean else m.inf)
  42. )(m.prod(float(x) for x in xs)**(1/len(xs)))
  43. }
  44. def openio(path, mode='r'):
  45. if path == '-':
  46. if mode == 'r':
  47. return os.fdopen(os.dup(sys.stdin.fileno()), 'r')
  48. else:
  49. return os.fdopen(os.dup(sys.stdout.fileno()), 'w')
  50. else:
  51. return open(path, mode)
  52. # integer fields
  53. class IntField(co.namedtuple('IntField', 'x')):
  54. __slots__ = ()
  55. def __new__(cls, x=0):
  56. if isinstance(x, IntField):
  57. return x
  58. if isinstance(x, str):
  59. try:
  60. x = int(x, 0)
  61. except ValueError:
  62. # also accept +-∞ and +-inf
  63. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  64. x = m.inf
  65. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  66. x = -m.inf
  67. else:
  68. raise
  69. assert isinstance(x, int) or m.isinf(x), x
  70. return super().__new__(cls, x)
  71. def __str__(self):
  72. if self.x == m.inf:
  73. return '∞'
  74. elif self.x == -m.inf:
  75. return '-∞'
  76. else:
  77. return str(self.x)
  78. def __int__(self):
  79. assert not m.isinf(self.x)
  80. return self.x
  81. def __float__(self):
  82. return float(self.x)
  83. none = '%7s' % '-'
  84. def table(self):
  85. return '%7s' % (self,)
  86. diff_none = '%7s' % '-'
  87. diff_table = table
  88. def diff_diff(self, other):
  89. new = self.x if self else 0
  90. old = other.x if other else 0
  91. diff = new - old
  92. if diff == +m.inf:
  93. return '%7s' % '+∞'
  94. elif diff == -m.inf:
  95. return '%7s' % '-∞'
  96. else:
  97. return '%+7d' % diff
  98. def ratio(self, other):
  99. new = self.x if self else 0
  100. old = other.x if other else 0
  101. if m.isinf(new) and m.isinf(old):
  102. return 0.0
  103. elif m.isinf(new):
  104. return +m.inf
  105. elif m.isinf(old):
  106. return -m.inf
  107. elif not old and not new:
  108. return 0.0
  109. elif not old:
  110. return 1.0
  111. else:
  112. return (new-old) / old
  113. def __add__(self, other):
  114. return IntField(self.x + other.x)
  115. def __sub__(self, other):
  116. return IntField(self.x - other.x)
  117. def __mul__(self, other):
  118. return IntField(self.x * other.x)
  119. def __lt__(self, other):
  120. return self.x < other.x
  121. def __gt__(self, other):
  122. return self.__class__.__lt__(other, self)
  123. def __le__(self, other):
  124. return not self.__gt__(other)
  125. def __ge__(self, other):
  126. return not self.__lt__(other)
  127. # float fields
  128. class FloatField(co.namedtuple('FloatField', 'x')):
  129. __slots__ = ()
  130. def __new__(cls, x=0.0):
  131. if isinstance(x, FloatField):
  132. return x
  133. if isinstance(x, str):
  134. try:
  135. x = float(x)
  136. except ValueError:
  137. # also accept +-∞ and +-inf
  138. if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x):
  139. x = m.inf
  140. elif re.match('^\s*-\s*(?:∞|inf)\s*$', x):
  141. x = -m.inf
  142. else:
  143. raise
  144. assert isinstance(x, float), x
  145. return super().__new__(cls, x)
  146. def __str__(self):
  147. if self.x == m.inf:
  148. return '∞'
  149. elif self.x == -m.inf:
  150. return '-∞'
  151. else:
  152. return '%.1f' % self.x
  153. def __float__(self):
  154. return float(self.x)
  155. none = IntField.none
  156. table = IntField.table
  157. diff_none = IntField.diff_none
  158. diff_table = IntField.diff_table
  159. diff_diff = IntField.diff_diff
  160. ratio = IntField.ratio
  161. __add__ = IntField.__add__
  162. __sub__ = IntField.__sub__
  163. __mul__ = IntField.__mul__
  164. __lt__ = IntField.__lt__
  165. __gt__ = IntField.__gt__
  166. __le__ = IntField.__le__
  167. __ge__ = IntField.__ge__
  168. # fractional fields, a/b
  169. class FracField(co.namedtuple('FracField', 'a,b')):
  170. __slots__ = ()
  171. def __new__(cls, a=0, b=None):
  172. if isinstance(a, FracField) and b is None:
  173. return a
  174. if isinstance(a, str) and b is None:
  175. a, b = a.split('/', 1)
  176. if b is None:
  177. b = a
  178. return super().__new__(cls, IntField(a), IntField(b))
  179. def __str__(self):
  180. return '%s/%s' % (self.a, self.b)
  181. def __float__(self):
  182. return float(self.a)
  183. none = '%11s %7s' % ('-', '-')
  184. def table(self):
  185. if not self.b.x:
  186. return self.none
  187. t = self.a.x/self.b.x
  188. return '%11s %7s' % (
  189. self,
  190. '∞%' if t == +m.inf
  191. else '-∞%' if t == -m.inf
  192. else '%.1f%%' % (100*t))
  193. diff_none = '%11s' % '-'
  194. def diff_table(self):
  195. if not self.b.x:
  196. return self.diff_none
  197. return '%11s' % (self,)
  198. def diff_diff(self, other):
  199. new_a, new_b = self if self else (IntField(0), IntField(0))
  200. old_a, old_b = other if other else (IntField(0), IntField(0))
  201. return '%11s' % ('%s/%s' % (
  202. new_a.diff_diff(old_a).strip(),
  203. new_b.diff_diff(old_b).strip()))
  204. def ratio(self, other):
  205. new_a, new_b = self if self else (IntField(0), IntField(0))
  206. old_a, old_b = other if other else (IntField(0), IntField(0))
  207. new = new_a.x/new_b.x if new_b.x else 1.0
  208. old = old_a.x/old_b.x if old_b.x else 1.0
  209. return new - old
  210. def __add__(self, other):
  211. return FracField(self.a + other.a, self.b + other.b)
  212. def __sub__(self, other):
  213. return FracField(self.a - other.a, self.b - other.b)
  214. def __mul__(self, other):
  215. return FracField(self.a * other.a, self.b + other.b)
  216. def __lt__(self, other):
  217. self_r = self.a.x/self.b.x if self.b.x else -m.inf
  218. other_r = other.a.x/other.b.x if other.b.x else -m.inf
  219. return self_r < other_r
  220. def __gt__(self, other):
  221. return self.__class__.__lt__(other, self)
  222. def __le__(self, other):
  223. return not self.__gt__(other)
  224. def __ge__(self, other):
  225. return not self.__lt__(other)
  226. # available types
  227. TYPES = [IntField, FloatField, FracField]
  228. def homogenize(results, *,
  229. by=None,
  230. fields=None,
  231. renames=[],
  232. define={},
  233. types=None,
  234. **_):
  235. results = results.copy()
  236. # rename fields?
  237. if renames:
  238. for r in results:
  239. # make a copy so renames can overlap
  240. r_ = {}
  241. for new_k, old_k in renames:
  242. if old_k in r:
  243. r_[new_k] = r[old_k]
  244. r.update(r_)
  245. # filter by matching defines
  246. if define:
  247. results_ = []
  248. for r in results:
  249. if all(k in r and r[k] in vs for k, vs in define):
  250. results_.append(r)
  251. results = results_
  252. # if fields not specified, try to guess from data
  253. if fields is None:
  254. fields = co.OrderedDict()
  255. for r in results:
  256. for k, v in r.items():
  257. if by is not None and k in by:
  258. continue
  259. types_ = []
  260. for type in fields.get(k, TYPES):
  261. try:
  262. type(v)
  263. types_.append(type)
  264. except ValueError:
  265. pass
  266. fields[k] = types_
  267. fields = list(k for k,v in fields.items() if v)
  268. # infer 'by' fields?
  269. if by is None:
  270. by = co.OrderedDict()
  271. for r in results:
  272. # also ignore None keys, these are introduced by csv.DictReader
  273. # when header + row mismatch
  274. by.update((k, True) for k in r.keys()
  275. if k is not None
  276. and k not in fields
  277. and not any(k == old_k for _, old_k in renames))
  278. by = list(by.keys())
  279. # go ahead and clean up none values, these can have a few forms
  280. results_ = []
  281. for r in results:
  282. results_.append({
  283. k: r[k] for k in it.chain(by, fields)
  284. if r.get(k) is not None and not (
  285. isinstance(r[k], str)
  286. and re.match('^\s*[+-]?\s*$', r[k]))})
  287. results = results_
  288. # find best type for all fields
  289. if types is None:
  290. def is_type(x, type):
  291. try:
  292. type(x)
  293. return True
  294. except ValueError:
  295. return False
  296. types = {}
  297. for k in fields:
  298. for type in TYPES:
  299. if all(k not in r or is_type(r[k], type) for r in results_):
  300. types[k] = type
  301. break
  302. else:
  303. print("no type matches field %r?" % k)
  304. sys.exit(-1)
  305. # homogenize types
  306. for r in results:
  307. for k in fields:
  308. if k in r:
  309. r[k] = types[k](r[k])
  310. return by, fields, types, results
  311. def fold(results, *,
  312. by=[],
  313. fields=[],
  314. types=None,
  315. ops={},
  316. **_):
  317. folding = co.OrderedDict()
  318. for r in results:
  319. name = tuple(r.get(k, '') for k in by)
  320. if name not in folding:
  321. folding[name] = {k: [] for k in fields}
  322. for k in fields:
  323. if k in r:
  324. folding[name][k].append(r[k])
  325. # merge fields, we need the count at this point for averages
  326. folded = []
  327. for name, r in folding.items():
  328. r_ = {}
  329. for k, vs in r.items():
  330. if vs:
  331. # sum fields by default
  332. op = OPS[ops.get(k, 'sum')]
  333. r_[k] = op(vs)
  334. # drop any rows without fields and any empty keys
  335. if r_:
  336. folded.append(dict(
  337. {k: v for k, v in zip(by, name) if v},
  338. **r_))
  339. # what is the type of merged fields?
  340. if types is not None:
  341. types_ = {}
  342. for k in fields:
  343. op = OPS[ops.get(k, 'sum')]
  344. types_[k] = op([types[k]()]).__class__
  345. if types is None:
  346. return folded
  347. else:
  348. return types_, folded
  349. def table(results, total, diff_results=None, diff_total=None, *,
  350. by=[],
  351. fields=[],
  352. types={},
  353. ops={},
  354. sort=None,
  355. reverse_sort=None,
  356. summary=False,
  357. all=False,
  358. percent=False,
  359. **_):
  360. all_, all = all, __builtins__.all
  361. table = {
  362. ','.join(r.get(k,'') for k in by): r
  363. for r in results}
  364. diff_table = {
  365. ','.join(r.get(k,'') for k in by): r
  366. for r in diff_results or []}
  367. # sort, note that python's sort is stable
  368. names = list(table.keys() | diff_table.keys())
  369. names.sort()
  370. if diff_results is not None:
  371. names.sort(key=lambda n: tuple(
  372. -types[k].ratio(
  373. table.get(n,{}).get(k),
  374. diff_table.get(n,{}).get(k))
  375. for k in fields))
  376. if sort:
  377. names.sort(key=lambda n: tuple(
  378. (table[n][k],) if k in table.get(n,{}) else ()
  379. for k in sort),
  380. reverse=True)
  381. elif reverse_sort:
  382. names.sort(key=lambda n: tuple(
  383. (table[n][k],) if k in table.get(n,{}) else ()
  384. for k in reverse_sort),
  385. reverse=False)
  386. # print header
  387. if not summary:
  388. title = '%s%s' % (
  389. ','.join(by),
  390. ' (%d added, %d removed)' % (
  391. sum(1 for n in table if n not in diff_table),
  392. sum(1 for n in diff_table if n not in table))
  393. if diff_results is not None and not percent else '')
  394. name_width = max(it.chain([23, len(title)], (len(n) for n in names)))
  395. else:
  396. title = ''
  397. name_width = 23
  398. name_width = 4*((name_width+1+4-1)//4)-1
  399. print('%-*s ' % (name_width, title), end='')
  400. if diff_results is None:
  401. widths = [
  402. 4*((max(len(types[k].none), len(k))+1+4-1)//4)-1
  403. for k in fields]
  404. print(' %s' % (
  405. ' '.join(k.rjust(w) for w, k in zip(widths, fields))))
  406. elif percent:
  407. widths = [
  408. 4*((max(len(types[k].diff_none), len(k))+1+4-1)//4)-1
  409. for k in fields]
  410. print(' %s' % (
  411. ' '.join(k.rjust(w) for w, k in zip(widths, fields))))
  412. else:
  413. widths = [
  414. 4*((max(len(types[k].diff_none), 1+len(k))+1+4-1)//4)-1
  415. for k in fields]
  416. print(' %s %s %s' % (
  417. ' '.join(('o'+k).rjust(w) for w, k in zip(widths, fields)),
  418. ' '.join(('n'+k).rjust(w) for w, k in zip(widths, fields)),
  419. ' '.join(('d'+k).rjust(w) for w, k in zip(widths, fields))))
  420. # print entries
  421. if not summary:
  422. for name in names:
  423. r = table.get(name, {})
  424. if diff_results is not None:
  425. diff_r = diff_table.get(name, {})
  426. ratios = [types[k].ratio(r.get(k), diff_r.get(k))
  427. for k in fields]
  428. if not any(ratios) and not all_:
  429. continue
  430. print('%-*s ' % (name_width, name), end='')
  431. if diff_results is None:
  432. print(' %s' % (
  433. ' '.join(
  434. (r[k].table()
  435. if k in r
  436. else types[k].none).rjust(w)
  437. for w, k in zip(widths, fields))))
  438. elif percent:
  439. print(' %s%s' % (
  440. ' '.join(
  441. (r[k].diff_table().rjust(w)
  442. if k in r
  443. else types[k].diff_none).rjust(w)
  444. for w, k in zip(widths, fields)),
  445. ' (%s)' % ', '.join(
  446. '+∞%' if t == +m.inf
  447. else '-∞%' if t == -m.inf
  448. else '%+.1f%%' % (100*t)
  449. for t in ratios)))
  450. else:
  451. print(' %s %s %s%s' % (
  452. ' '.join(
  453. (diff_r[k].diff_table()
  454. if k in diff_r
  455. else types[k].diff_none).rjust(w)
  456. for w, k in zip(widths, fields)),
  457. ' '.join(
  458. (r[k].diff_table()
  459. if k in r
  460. else types[k].diff_none).rjust(w)
  461. for w, k in zip(widths, fields)),
  462. ' '.join(
  463. (types[k].diff_diff(r.get(k), diff_r.get(k))
  464. if k in r or k in diff_r
  465. else types[k].diff_none).rjust(w)
  466. for w, k in zip(widths, fields)),
  467. ' (%s)' % ', '.join(
  468. '+∞%' if t == +m.inf
  469. else '-∞%' if t == -m.inf
  470. else '%+.1f%%' % (100*t)
  471. for t in ratios
  472. if t)
  473. if any(ratios) else ''))
  474. # print total
  475. r = total
  476. if diff_total is not None:
  477. diff_r = diff_total
  478. ratios = [types[k].ratio(r.get(k), diff_r.get(k))
  479. for k in fields]
  480. print('%-*s ' % (name_width, 'TOTAL'), end='')
  481. if diff_results is None:
  482. print(' %s' % (
  483. ' '.join(
  484. (r[k].table()
  485. if k in r
  486. else types[k].none).rjust(w)
  487. for w, k in zip(widths, fields))))
  488. elif percent:
  489. print(' %s%s' % (
  490. ' '.join(
  491. (r[k].diff_table().rjust(w)
  492. if k in r
  493. else types[k].diff_none).rjust(w)
  494. for w, k in zip(widths, fields)),
  495. ' (%s)' % ', '.join(
  496. '+∞%' if t == +m.inf
  497. else '-∞%' if t == -m.inf
  498. else '%+.1f%%' % (100*t)
  499. for t in ratios)))
  500. else:
  501. print(' %s %s %s%s' % (
  502. ' '.join(
  503. (diff_r[k].diff_table()
  504. if k in diff_r
  505. else types[k].diff_none).rjust(w)
  506. for w, k in zip(widths, fields)),
  507. ' '.join(
  508. (r[k].diff_table()
  509. if k in r
  510. else types[k].diff_none).rjust(w)
  511. for w, k in zip(widths, fields)),
  512. ' '.join(
  513. (types[k].diff_diff(r.get(k), diff_r.get(k))
  514. if k in r or k in diff_r
  515. else types[k].diff_none).rjust(w)
  516. for w, k in zip(widths, fields)),
  517. ' (%s)' % ', '.join(
  518. '+∞%' if t == +m.inf
  519. else '-∞%' if t == -m.inf
  520. else '%+.1f%%' % (100*t)
  521. for t in ratios
  522. if t)
  523. if any(ratios) else ''))
  524. def main(csv_paths, *,
  525. by=None,
  526. fields=None,
  527. define=[],
  528. **args):
  529. # separate out renames
  530. renames = [k.split('=', 1)
  531. for k in it.chain(by or [], fields or [])
  532. if '=' in k]
  533. if by is not None:
  534. by = [k.split('=', 1)[0] for k in by]
  535. if fields is not None:
  536. fields = [k.split('=', 1)[0] for k in fields]
  537. # figure out merge operations
  538. ops = {}
  539. for m in OPS.keys():
  540. for k in args.get(m, []):
  541. if k in ops:
  542. print("conflicting op for field %r?" % k)
  543. sys.exit(-1)
  544. ops[k] = m
  545. # rename ops?
  546. if renames:
  547. ops_ = {}
  548. for new_k, old_k in renames:
  549. if old_k in ops:
  550. ops_[new_k] = ops[old_k]
  551. ops.update(ops_)
  552. # find CSV files
  553. paths = []
  554. for path in csv_paths:
  555. if os.path.isdir(path):
  556. path = path + '/*.csv'
  557. for path in glob.glob(path):
  558. paths.append(path)
  559. if not paths:
  560. print('no .csv files found in %r?' % csv_paths)
  561. sys.exit(-1)
  562. results = []
  563. for path in paths:
  564. try:
  565. with openio(path) as f:
  566. reader = csv.DictReader(f, restval='')
  567. for r in reader:
  568. results.append(r)
  569. except FileNotFoundError:
  570. pass
  571. # homogenize
  572. by, fields, types, results = homogenize(results,
  573. by=by, fields=fields, renames=renames, define=define)
  574. # fold for total, note we do this with the raw data to avoid
  575. # issues with lossy operations
  576. total = fold(results, fields=fields, ops=ops)
  577. total = total[0] if total else {}
  578. # fold to remove duplicates
  579. types_, results = fold(results, by=by, fields=fields, types=types, ops=ops)
  580. # write results to CSV
  581. if args.get('output'):
  582. with openio(args['output'], 'w') as f:
  583. writer = csv.DictWriter(f, by + fields)
  584. writer.writeheader()
  585. for r in results:
  586. writer.writerow(r)
  587. # find previous results?
  588. if args.get('diff'):
  589. diff_results = []
  590. try:
  591. with openio(args['diff']) as f:
  592. reader = csv.DictReader(f, restval='')
  593. for r in reader:
  594. diff_results.append(r)
  595. except FileNotFoundError:
  596. pass
  597. # homogenize
  598. _, _, _, diff_results = homogenize(diff_results,
  599. by=by, fields=fields, renames=renames, define=define, types=types)
  600. # fold for total, note we do this with the raw data to avoid
  601. # issues with lossy operations
  602. diff_total = fold(diff_results, fields=fields, ops=ops)
  603. diff_total = diff_total[0] if diff_total else {}
  604. # fold to remove duplicates
  605. diff_results = fold(diff_results, by=by, fields=fields, ops=ops)
  606. # print table
  607. if not args.get('quiet'):
  608. table(
  609. results,
  610. total,
  611. diff_results if args.get('diff') else None,
  612. diff_total if args.get('diff') else None,
  613. by=by,
  614. fields=fields,
  615. types=types_,
  616. ops=ops,
  617. **args)
  618. if __name__ == "__main__":
  619. import argparse
  620. import sys
  621. parser = argparse.ArgumentParser(
  622. description="Summarize measurements in CSV files.")
  623. parser.add_argument(
  624. 'csv_paths',
  625. nargs='*',
  626. default=CSV_PATHS,
  627. help="Description of where to find *.csv files. May be a directory "
  628. "or list of paths. Defaults to %r." % CSV_PATHS)
  629. parser.add_argument(
  630. '-q', '--quiet',
  631. action='store_true',
  632. help="Don't show anything, useful with -o.")
  633. parser.add_argument(
  634. '-o', '--output',
  635. help="Specify CSV file to store results.")
  636. parser.add_argument(
  637. '-d', '--diff',
  638. help="Specify CSV file to diff against.")
  639. parser.add_argument(
  640. '-a', '--all',
  641. action='store_true',
  642. help="Show all, not just the ones that changed.")
  643. parser.add_argument(
  644. '-p', '--percent',
  645. action='store_true',
  646. help="Only show percentage change, not a full diff.")
  647. parser.add_argument(
  648. '-b', '--by',
  649. action='append',
  650. help="Group by these fields. All other fields will be merged as "
  651. "needed. Can rename fields with new_name=old_name.")
  652. parser.add_argument(
  653. '-f', '--fields',
  654. action='append',
  655. help="Use these fields. Can rename fields with new_name=old_name.")
  656. parser.add_argument(
  657. '-D', '--define',
  658. action='append',
  659. type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)),
  660. help="Only include rows where this field is this value. May include "
  661. "comma-separated options.")
  662. parser.add_argument(
  663. '--sum',
  664. action='append',
  665. help="Add these fields (the default).")
  666. parser.add_argument(
  667. '--prod',
  668. action='append',
  669. help="Multiply these fields.")
  670. parser.add_argument(
  671. '--min',
  672. action='append',
  673. help="Take the minimum of these fields.")
  674. parser.add_argument(
  675. '--max',
  676. action='append',
  677. help="Take the maximum of these fields.")
  678. parser.add_argument(
  679. '--mean',
  680. action='append',
  681. help="Average these fields.")
  682. parser.add_argument(
  683. '--stddev',
  684. action='append',
  685. help="Find the standard deviation of these fields.")
  686. parser.add_argument(
  687. '--gmean',
  688. action='append',
  689. help="Find the geometric mean of these fields.")
  690. parser.add_argument(
  691. '--gstddev',
  692. action='append',
  693. help="Find the geometric standard deviation of these fields.")
  694. parser.add_argument(
  695. '-s', '--sort',
  696. action='append',
  697. help="Sort by these fields.")
  698. parser.add_argument(
  699. '-S', '--reverse-sort',
  700. action='append',
  701. help="Sort by these fields, but backwards.")
  702. parser.add_argument(
  703. '-Y', '--summary',
  704. action='store_true',
  705. help="Only show the totals.")
  706. sys.exit(main(**{k: v
  707. for k, v in vars(parser.parse_intermixed_args()).items()
  708. if v is not None}))