From 2d41d64c5b35420a16bd132fe58f60be16d7ff63 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 26 Jan 2024 13:15:40 +0000 Subject: [PATCH] Deployed 41fd0ea to master with MkDocs 1.5.3 and mike 2.0.0 --- master/reference/nimlite/index.html | 2 +- master/search/search_index.json | 2 +- master/sitemap.xml.gz | Bin 401 -> 401 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 418 -> 418 bytes .../tablite/__pycache__/base.cpython-310.pyc | Bin 58428 -> 58428 bytes .../__pycache__/config.cpython-310.pyc | Bin 2722 -> 2722 bytes .../tablite/__pycache__/core.cpython-310.pyc | Bin 31108 -> 31108 bytes .../__pycache__/datatypes.cpython-310.pyc | Bin 31028 -> 31028 bytes .../tablite/__pycache__/diff.cpython-310.pyc | Bin 2793 -> 2793 bytes .../__pycache__/export_utils.cpython-310.pyc | Bin 8528 -> 8528 bytes .../file_reader_utils.cpython-310.pyc | Bin 7957 -> 7957 bytes .../__pycache__/groupby_utils.cpython-310.pyc | Bin 7407 -> 7407 bytes .../__pycache__/groupbys.cpython-310.pyc | Bin 6055 -> 6055 bytes .../__pycache__/import_utils.cpython-310.pyc | Bin 18168 -> 18168 bytes .../__pycache__/imputation.cpython-310.pyc | Bin 7125 -> 7125 bytes .../tablite/__pycache__/joins.cpython-310.pyc | Bin 11493 -> 11493 bytes .../__pycache__/lookup.cpython-310.pyc | Bin 4169 -> 4169 bytes .../tablite/__pycache__/match.cpython-310.pyc | Bin 2693 -> 2693 bytes .../tablite/__pycache__/merge.cpython-310.pyc | Bin 1259 -> 1259 bytes .../__pycache__/mp_utils.cpython-310.pyc | Bin 2903 -> 2903 bytes .../__pycache__/nimlite.cpython-310.pyc | Bin 7884 -> 7883 bytes .../__pycache__/pivots.cpython-310.pyc | Bin 7549 -> 7549 bytes .../tablite/__pycache__/redux.cpython-310.pyc | Bin 8934 -> 8934 bytes .../__pycache__/reindex.cpython-310.pyc | Bin 1402 -> 1402 bytes .../__pycache__/sort_utils.cpython-310.pyc | Bin 7242 -> 7242 bytes .../__pycache__/sortation.cpython-310.pyc | Bin 5018 -> 5018 bytes .../tablite/__pycache__/utils.cpython-310.pyc | Bin 13076 -> 13076 bytes .../__pycache__/version.cpython-310.pyc | Bin 381 -> 381 bytes .../__pycache__/__init__.cpython-310.pyc | Bin 155 -> 155 bytes .../funcs/column_selector/sliceconv.nim | 32 +++++++++--------- master/tablite/_nimlite/nimlite.so | Bin 1404000 -> 1408096 bytes master/tablite/nimlite.py | 2 +- master/tablite/version.py | 2 +- 33 files changed, 20 insertions(+), 20 deletions(-) diff --git a/master/reference/nimlite/index.html b/master/reference/nimlite/index.html index 5e6be526..234d2984 100644 --- a/master/reference/nimlite/index.html +++ b/master/reference/nimlite/index.html @@ -1992,7 +1992,7 @@

251 252 253
def column_select(table, cols, tqdm=_tqdm, TaskManager=TaskManager):
-    with tqdm(total=100, desc="column select", bar_format='{desc}: {percentage:3.0f}%|{bar}{r_bar}') as pbar:
+    with tqdm(total=100, desc="column select", bar_format='{desc}: {percentage:.1f}%|{bar}{r_bar}') as pbar:
         T = type(table)
         dir_pid = Config.workdir / Config.pid
 
diff --git a/master/search/search_index.json b/master/search/search_index.json
index 8fba3f3a..d77649cb 100644
--- a/master/search/search_index.json
+++ b/master/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Tablite","text":""},{"location":"#contents","title":"Contents","text":"
  • introduction
  • installation
  • feature overview
  • tutorial
  • latest updates
  • credits
"},{"location":"#introduction","title":"Introduction","text":"

Tablite seeks to be the go-to library for manipulating tabular data with an api that is as close in syntax to pure python as possible.

"},{"location":"#even-smaller-memory-footprint","title":"Even smaller memory footprint","text":"

Tablite uses numpys fileformat as a backend with strong abstraction, so that copy, append & repetition of data is handled in pages. This is imperative for incremental data processing.

Tablite tests for memory footprint. One test compares the memory footprint of 10,000,000 integers where tablite will use < 1 Mb RAM in contrast to python which will require around 133.7 Mb of RAM (1M lists with 10 integers). Tablite also tests to assure that working with 1Tb of data is tolerable.

Tablite achieves this minimal memory footprint by using a temporary storage set in config.Config.workdir as tempfile.gettempdir()/tablite-tmp. If your OS (windows/linux/mac) sits on a SSD this will benefit from high IOPS and permit slices of 9,000,000,000 rows in less than a second.

"},{"location":"#multiprocessing-enabled-by-default","title":"Multiprocessing enabled by default","text":"

Tablite uses numpy whereever possible and applies multiprocessing for bypassing the GIL on all major operations. CSV import is performed in C through using nims compiler and is as fast the hardware allows.

"},{"location":"#all-algorithms-have-been-reworked-to-respect-memory-limits","title":"All algorithms have been reworked to respect memory limits","text":"

Tablite respects the limits of free memory by tagging the free memory and defining task size before each memory intensive task is initiated (join, groupby, data import, etc). If you still run out of memory you may try to reduce the config.Config.PAGE_SIZE and rerun your program.

"},{"location":"#100-support-for-all-python-datatypes","title":"100% support for all python datatypes","text":"

Tablite wants to make it easy for you to work with data. tablite.Table's behave like a dict with lists:

my_table[column name] = [... data ...].

Tablite uses datatype mapping to native numpy types where possible and uses type mapping for non-native types such as timedelta, None, date, time\u2026 e.g. what you put in, is what you get out. This is inspired by bank python.

"},{"location":"#light-weight","title":"Light weight","text":"

Tablite is ~200 kB.

"},{"location":"#helpful","title":"Helpful","text":"

Tablite wants you to be productive, so a number of helpers are available.

  • Table.import_file to import csv*, tsv, txt, xls, xlsx, xlsm, ods, zip and logs. There is automatic type detection (see tutorial.ipynb )
  • To peek into any supported file use get_headers which shows the first 10 rows.
  • Use mytable.rows and mytable.columns to iterate over rows or columns.
  • Create multi-key .index for quick lookups.
  • Perform multi-key .sort,
  • Filter using .any and .all to select specific rows.
  • use multi-key .lookup and .join to find data across tables.
  • Perform .groupby and reorganise data as a .pivot table with max, min, sum, first, last, count, unique, average, st.deviation, median and mode
  • Append / concatenate tables with += which automatically sorts out the columns - even if they're not in perfect order.
  • Should you tables be similar but not the identical you can use .stack to \"stack\" tables on top of each other

If you're still missing something add it to the wishlist

"},{"location":"#installation","title":"Installation","text":"

Get it from pypi:

Install: pip install tablite Usage: >>> from tablite import Table

"},{"location":"#build-test","title":"Build & test","text":"
install nim >= 2.0.0\nchmod +x ./build_nim.sh\nrun ./build_nim.sh\n

Should the default nim not be your desired taste, please use nims environment manager (atlas) and run source nim-2.0.0/activate.sh on UNIX or nim-2.0.0/activate.bat on windows.

install python >= 3.8\npython -m venv /your/venv/dir\nactivate /your/venv/dir\npip install -r requirements.txt\npip install -r requirements_for_testing.py\npytest ./tests\n
"},{"location":"#feature-overview","title":"Feature overview","text":"want to... this way... loop over rows [ row for row in table.rows ] loop over columns [ table[col_name] for col_name in table.columns ] slice myslice = table['A', 'B', slice(0,None,15)] get column by name my_table['A'] get row by index my_table[9_000_000_001] value update mytable['A'][2] = new value update w. list comprehension mytable['A'] = [ x*x for x in mytable['A'] if x % 2 != 0 ] join a_join = numbers.join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'], kind='left') lookup travel_plan = friends.lookup(bustable, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop')) groupby group_by = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)]) pivot table my_pivot = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False) index indices = old_table.index(*old_table.columns) sort lookup1_sorted = lookup_1.sort(**{'time': True, 'name':False, \"sort_mode\":'unix'}) filter true, false = unfiltered.filter( [{\"column1\": 'a', \"criteria\":\">=\", 'value2':3}, ... more criteria ... ], filter_type='all' ) find any any_even_rows = mytable.any('A': lambda x : x%2==0, 'B': lambda x > 0) find all all_even_rows = mytable.all('A': lambda x : x%2==0, 'B': lambda x > 0) to json json_str = my_table.to_json() from json Table.from_json(json_str)"},{"location":"#tutorial","title":"Tutorial","text":"

To learn more see the tutorial.ipynb (Jupyter notebook)

"},{"location":"#latest-updates","title":"Latest updates","text":"

See changelog.md

"},{"location":"#credits","title":"Credits","text":"
  • Martynas Kaunas - GroupBy functionality.
  • Audrius Kulikajevas - Edge case testing / various bugs, Jupyter notebook integration.
  • Sergej Sinkarenko - various bugs.
  • Ovidijus Grigas - various bugs, documentation.
  • Lori Cooper - spell checking.
"},{"location":"benchmarks/","title":"Benchmarks","text":"In\u00a0[2]: Copied!
import psutil, os, gc, shutil, tempfile\nfrom pathlib import Path\nfrom time import perf_counter, time\nfrom tablite import Table\nfrom tablite.datasets import synthetic_order_data\nfrom tablite.config import Config\n\nConfig.TQDM_DISABLE = True\n
import psutil, os, gc, shutil, tempfile from pathlib import Path from time import perf_counter, time from tablite import Table from tablite.datasets import synthetic_order_data from tablite.config import Config Config.TQDM_DISABLE = True In\u00a0[3]: Copied!
process = psutil.Process(os.getpid())\n\ndef make_tables(sizes=[1,2,5,10,20,50]):\n    # The last tables are too big for RAM (~24Gb), so I create subtables of 1M rows and append them.\n    t = synthetic_order_data(Config.PAGE_SIZE)\n    real, flat = t.nbytes()\n    print(f\"Table {len(t):,} rows is {real/1e6:,.0f} Mb on disk\")\n\n    tables = [t]  # 1M rows.\n\n    last = 1\n    t2 = t.copy()\n    for i in sizes[1:]:\n        t2 = t2.copy()\n        for _ in range(i-last):\n            t2 += synthetic_order_data(Config.PAGE_SIZE)  # these are all unique\n        last = i\n        real, flat = t2.nbytes()\n        tables.append(t2)\n        print(f\"Table {len(t2):,} rows is {real/1e6:,.0f} Mb on disk\")\n    return tables\n\ntables = make_tables()\n
process = psutil.Process(os.getpid()) def make_tables(sizes=[1,2,5,10,20,50]): # The last tables are too big for RAM (~24Gb), so I create subtables of 1M rows and append them. t = synthetic_order_data(Config.PAGE_SIZE) real, flat = t.nbytes() print(f\"Table {len(t):,} rows is {real/1e6:,.0f} Mb on disk\") tables = [t] # 1M rows. last = 1 t2 = t.copy() for i in sizes[1:]: t2 = t2.copy() for _ in range(i-last): t2 += synthetic_order_data(Config.PAGE_SIZE) # these are all unique last = i real, flat = t2.nbytes() tables.append(t2) print(f\"Table {len(t2):,} rows is {real/1e6:,.0f} Mb on disk\") return tables tables = make_tables()
Table 1,000,000 rows is 256 Mb on disk\nTable 2,000,000 rows is 512 Mb on disk\nTable 5,000,000 rows is 1,280 Mb on disk\nTable 10,000,000 rows is 2,560 Mb on disk\nTable 20,000,000 rows is 5,120 Mb on disk\nTable 50,000,000 rows is 12,800 Mb on disk\n

The values in the tables above are all unique!

In\u00a0[4]: Copied!
tables[-1]\n
tables[-1] Out[4]: ~#1234567891011 0114014953182952021-10-06T00:00:0050814119375C3-4HGQ21\u00b0XYZ1.244647268201734421.367107051830455 129320231372182021-08-26T00:00:005007718568C5-5FZU0\u00b00.55294485347516132.6980406874392537 2312569602250812021-12-21T00:00:0050197029074C2-3GTK6\u00b0XYZ1.99739754559065617.513164305723787 3414012777817432021-08-23T00:00:0050818024969C4-3BYP6\u00b0XYZ0.047497125538289577.388171617130485 459426667674262021-07-31T00:00:0050307113074C5-2CCC21\u00b0ABC1.0219215027612885.21324123446987 5612186131851272021-12-01T00:00:0050484117249C5-4WGT21\u00b00.2038764258434556712.190974436133764 676070424343982021-11-29T00:00:0050578011564C2-3LUL0\u00b0XYZ2.2367835158480444.340628097363572.......................................49,999,9939999946602693775472021-09-17T00:00:005015409706C4-3AHQ21\u00b0XYZ0.083216645843125856.56780297752790549,999,9949999955709798646952021-08-01T00:00:0050149125006C1-2FWH6\u00b01.04763923662266419.50710544462706549,999,9959999963551956078252021-07-29T00:00:0050007026992C4-3GVG21\u00b02.20440816560941411.2706443974284949,999,99699999720762240577282021-10-16T00:00:0050950113339C5-4NKS0\u00b02.1593110498135494.21575620046596149,999,9979999986577247891352021-12-21T00:00:0050069114747C2-4LYGNone1.64809640191698683.094420483625827349,999,9989999999775312438842021-12-02T00:00:0050644129345C2-5DRH6\u00b02.30911421692753110.82706867207146849,999,999100000012290713920652021-08-23T00:00:0050706119732C4-5AGB6\u00b00.488871405593691630.8580085696389939 In\u00a0[5]: Copied!
def save_load_benchmarks(tables):\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n\n    results = Table()\n    results.add_columns('rows', 'save (sec)', 'load (sec)')\n    for t in tables:\n        fn = tmp / f'{len(t)}.tpz'\n        start = perf_counter()\n        t.save(fn)\n        end = perf_counter()\n        save = round(end-start,3)\n        assert fn.exists()\n        \n        \n        start = perf_counter()\n        t2 = Table.load(fn)\n        end = perf_counter()\n        load = round(end-start,3)\n        print(f\"saving {len(t):,} rows ({fn.stat().st_size/1e6:,.0f} Mb) took {save:,.3f} seconds. loading took {load:,.3f} seconds\")\n        del t2\n        fn.unlink()\n        results.add_rows(len(t), save, load)\n    \n    r = results\n    r['save r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['save (sec)']) ]\n    r['load r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['load (sec)'])]\n\n    return results\n
def save_load_benchmarks(tables): tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) results = Table() results.add_columns('rows', 'save (sec)', 'load (sec)') for t in tables: fn = tmp / f'{len(t)}.tpz' start = perf_counter() t.save(fn) end = perf_counter() save = round(end-start,3) assert fn.exists() start = perf_counter() t2 = Table.load(fn) end = perf_counter() load = round(end-start,3) print(f\"saving {len(t):,} rows ({fn.stat().st_size/1e6:,.0f} Mb) took {save:,.3f} seconds. loading took {load:,.3f} seconds\") del t2 fn.unlink() results.add_rows(len(t), save, load) r = results r['save r/sec'] = [int(a/b) if b!=0 else \"nil\" for a,b in zip(r['rows'], r['save (sec)']) ] r['load r/sec'] = [int(a/b) if b!=0 else \"nil\" for a,b in zip(r['rows'], r['load (sec)'])] return results In\u00a0[6]: Copied!
slb = save_load_benchmarks(tables)\n
slb = save_load_benchmarks(tables)
saving 1,000,000 rows (49 Mb) took 2.148 seconds. loading took 0.922 seconds\nsaving 2,000,000 rows (98 Mb) took 4.267 seconds. loading took 1.820 seconds\nsaving 5,000,000 rows (246 Mb) took 10.618 seconds. loading took 4.482 seconds\nsaving 10,000,000 rows (492 Mb) took 21.291 seconds. loading took 8.944 seconds\nsaving 20,000,000 rows (984 Mb) took 42.603 seconds. loading took 17.821 seconds\nsaving 50,000,000 rows (2,461 Mb) took 106.644 seconds. loading took 44.600 seconds\n
In\u00a0[7]: Copied!
slb\n
slb Out[7]: #rowssave (sec)load (sec)save r/secload r/sec 010000002.1480.9224655491084598 120000004.2671.824687131098901 2500000010.6184.4824708981115573 31000000021.2918.9444696821118067 42000000042.60317.8214694501122271 550000000106.64444.64688491121076

With various compression options

In\u00a0[8]: Copied!
def save_compression_benchmarks(t):\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n\n    import zipfile  # https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile\n    methods = [(None, zipfile.ZIP_STORED, \"zip stored\"), (None, zipfile.ZIP_LZMA, \"zip lzma\")]\n    methods += [(i, zipfile.ZIP_DEFLATED, \"zip deflated\") for i in range(0,10)]\n    methods += [(i, zipfile.ZIP_BZIP2, \"zip bzip2\") for i in range(1,10)]\n\n    results = Table()\n    results.add_columns('file size (Mb)', 'method', 'write (sec)', 'read (sec)')\n    for level, method, name in methods:\n        fn = tmp / f'{len(t)}.tpz'\n        start = perf_counter()  \n        t.save(fn, compression_method=method, compression_level=level)\n        end = perf_counter()\n        write = round(end-start,3)\n        assert fn.exists()\n        size = int(fn.stat().st_size/1e6)\n        # print(f\"{name}(level={level}): {len(t):,} rows ({size} Mb) took {write:,.3f} secconds to save\", end='')\n        \n        start = perf_counter()\n        t2 = Table.load(fn)\n        end = perf_counter()\n        read = round(end-start,3)\n        # print(f\" and {end-start:,.3} seconds to load\")\n        print(\".\", end='')\n        \n        del t2\n        fn.unlink()\n        results.add_rows(size, f\"{name}(level={level})\", write, read)\n        \n    \n    r = results\n    r.sort({'write (sec)':True})\n    r['write (rps)'] = [int(1_000_000/b) for b in r['write (sec)']]\n    r['read (rps)'] = [int(1_000_000/b) for b in r['read (sec)']]\n    return results\n
def save_compression_benchmarks(t): tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) import zipfile # https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile methods = [(None, zipfile.ZIP_STORED, \"zip stored\"), (None, zipfile.ZIP_LZMA, \"zip lzma\")] methods += [(i, zipfile.ZIP_DEFLATED, \"zip deflated\") for i in range(0,10)] methods += [(i, zipfile.ZIP_BZIP2, \"zip bzip2\") for i in range(1,10)] results = Table() results.add_columns('file size (Mb)', 'method', 'write (sec)', 'read (sec)') for level, method, name in methods: fn = tmp / f'{len(t)}.tpz' start = perf_counter() t.save(fn, compression_method=method, compression_level=level) end = perf_counter() write = round(end-start,3) assert fn.exists() size = int(fn.stat().st_size/1e6) # print(f\"{name}(level={level}): {len(t):,} rows ({size} Mb) took {write:,.3f} secconds to save\", end='') start = perf_counter() t2 = Table.load(fn) end = perf_counter() read = round(end-start,3) # print(f\" and {end-start:,.3} seconds to load\") print(\".\", end='') del t2 fn.unlink() results.add_rows(size, f\"{name}(level={level})\", write, read) r = results r.sort({'write (sec)':True}) r['write (rps)'] = [int(1_000_000/b) for b in r['write (sec)']] r['read (rps)'] = [int(1_000_000/b) for b in r['read (sec)']] return results In\u00a0[9]: Copied!
scb = save_compression_benchmarks(tables[0])\n
scb = save_compression_benchmarks(tables[0])
.....................
creating sort index:   0%|          | 0/1 [00:00<?, ?it/s]\rcreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 268.92it/s]\n
In\u00a0[10]: Copied!
scb[0:20]\n
scb[0:20] Out[10]: #file size (Mb)methodwrite (sec)read (sec)write (rps)read (rps) 0256zip stored(level=None)0.3960.47525252522105263 129zip lzma(level=None)95.1372.22810511448833 2256zip deflated(level=0)0.5350.59518691581680672 349zip deflated(level=1)2.150.9224651161084598 447zip deflated(level=2)2.2640.9124416961096491 543zip deflated(level=3)3.0490.833279761204819 644zip deflated(level=4)2.920.8623424651160092 742zip deflated(level=5)4.0340.8692478921150747 840zip deflated(level=6)8.5580.81168491250000 939zip deflated(level=7)13.6950.7787301912853471038zip deflated(level=8)56.9720.7921755212626261138zip deflated(level=9)122.6230.791815512642221229zip bzip2(level=1)15.1214.065661332460021329zip bzip2(level=2)16.0474.214623162373041429zip bzip2(level=3)16.8584.409593192268081529zip bzip2(level=4)17.6485.141566631945141629zip bzip2(level=5)18.6746.009535501664171729zip bzip2(level=6)19.4056.628515331508751829zip bzip2(level=7)19.9546.714501151489421929zip bzip2(level=8)20.5956.96148555143657

Conclusions

  • Fastest: zip stored with no compression takes handles
In\u00a0[11]: Copied!
def to_sql_benchmark(t, rows=1_000_000):\n    t2 = t[:rows]\n    write_start = time()\n    _ = t2.to_sql(name='1')\n    write_end = time()\n    write = round(write_end-write_start,3)\n    return ( t.to_sql.__name__, write, 0, len(t2), \"\" , \"\" )\n
def to_sql_benchmark(t, rows=1_000_000): t2 = t[:rows] write_start = time() _ = t2.to_sql(name='1') write_end = time() write = round(write_end-write_start,3) return ( t.to_sql.__name__, write, 0, len(t2), \"\" , \"\" ) In\u00a0[12]: Copied!
def to_json_benchmark(t, rows=1_000_000):\n    t2 = t[:rows]\n\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n    path = tmp / \"1.json\" \n    \n    write_start = time()\n    bytestr = t2.to_json()\n    with path.open('w') as fo:\n        fo.write(bytestr)\n    write_end = time()\n    write = round(write_end-write_start,3)\n\n    read_start = time()\n    with path.open('r') as fi:\n        _ = Table.from_json(fi.read())  # <-- JSON\n    read_end = time()\n    read = round(read_end-read_start,3)\n\n    return ( t.to_json.__name__, write, read, len(t2), int(path.stat().st_size/1e6), \"\" )\n
def to_json_benchmark(t, rows=1_000_000): t2 = t[:rows] tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) path = tmp / \"1.json\" write_start = time() bytestr = t2.to_json() with path.open('w') as fo: fo.write(bytestr) write_end = time() write = round(write_end-write_start,3) read_start = time() with path.open('r') as fi: _ = Table.from_json(fi.read()) # <-- JSON read_end = time() read = round(read_end-read_start,3) return ( t.to_json.__name__, write, read, len(t2), int(path.stat().st_size/1e6), \"\" ) In\u00a0[13]: Copied!
def f(t, args):\n    rows, c1, c1_kw, c2, c2_kw = args\n    t2 = t[:rows]\n\n    call = getattr(t2, c1)\n    assert callable(call)\n\n    write_start = time()\n    call(**c1_kw)\n    write_end = time()\n    write = round(write_end-write_start,3)\n\n    for _ in range(10):\n        gc.collect()\n\n    read_start = time()\n    if callable(c2):\n        c2(**c2_kw)\n    read_end = time()\n    read = round(read_end-read_start,3)\n\n    fn = c2_kw['path']\n    assert fn.exists()\n    fs = int(fn.stat().st_size/1e6)\n    config = {k:v for k,v in c2_kw.items() if k!= 'path'}\n\n    return ( c1, write, read, len(t2), fs , str(config))\n
def f(t, args): rows, c1, c1_kw, c2, c2_kw = args t2 = t[:rows] call = getattr(t2, c1) assert callable(call) write_start = time() call(**c1_kw) write_end = time() write = round(write_end-write_start,3) for _ in range(10): gc.collect() read_start = time() if callable(c2): c2(**c2_kw) read_end = time() read = round(read_end-read_start,3) fn = c2_kw['path'] assert fn.exists() fs = int(fn.stat().st_size/1e6) config = {k:v for k,v in c2_kw.items() if k!= 'path'} return ( c1, write, read, len(t2), fs , str(config)) In\u00a0[14]: Copied!
def import_export_benchmarks(tables):\n    Config.PROCESSING_MODE = Config.FALSE\n        \n    t = sorted(tables, key=lambda x: len(x), reverse=True)[0]\n    \n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)   \n\n    args = [\n        (   100_000, \"to_xlsx\", {'path': tmp/'1.xlsx'}, Table.from_file, {\"path\":tmp/'1.xlsx', \"sheet\":\"pyexcel_sheet1\"}),\n        (    50_000,  \"to_ods\",  {'path': tmp/'1.ods'}, Table.from_file, {\"path\":tmp/'1.ods', \"sheet\":\"pyexcel_sheet1\"} ),  # 50k rows, otherwise MemoryError.\n        ( 1_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv'}                           ),\n        ( 1_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}),\n        (10_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}),\n        ( 1_000_000,  \"to_tsv\",  {'path': tmp/'1.tsv'}, Table.from_file, {\"path\":tmp/'1.tsv'}                           ),\n        ( 1_000_000, \"to_text\",  {'path': tmp/'1.txt'}, Table.from_file, {\"path\":tmp/'1.txt'}                           ),\n        ( 1_000_000, \"to_html\", {'path': tmp/'1.html'}, Table.from_file, {\"path\":tmp/'1.html'}                          ),\n        ( 1_000_000, \"to_hdf5\", {'path': tmp/'1.hdf5'}, Table.from_file, {\"path\":tmp/'1.hdf5'}                          )\n    ]\n\n    results = Table()\n    results.add_columns('method', 'write (s)', 'read (s)', 'rows', 'size (Mb)', 'config')\n\n    results.add_rows( to_sql_benchmark(t) )\n    results.add_rows( to_json_benchmark(t) )\n\n    for arg in args:\n        if len(t)<arg[0]:\n            continue\n        print(\".\", end='')\n        try:\n            results.add_rows( f(t, arg) )\n        except MemoryError:\n            results.add_rows( arg[1], \"Memory Error\", \"NIL\", args[0], \"NIL\", \"N/A\")\n    \n    r = results\n    r['read r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['read (s)']) ]\n    r['write r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['write (s)'])]\n\n    shutil.rmtree(tmp)\n    return results\n
def import_export_benchmarks(tables): Config.PROCESSING_MODE = Config.FALSE t = sorted(tables, key=lambda x: len(x), reverse=True)[0] tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) args = [ ( 100_000, \"to_xlsx\", {'path': tmp/'1.xlsx'}, Table.from_file, {\"path\":tmp/'1.xlsx', \"sheet\":\"pyexcel_sheet1\"}), ( 50_000, \"to_ods\", {'path': tmp/'1.ods'}, Table.from_file, {\"path\":tmp/'1.ods', \"sheet\":\"pyexcel_sheet1\"} ), # 50k rows, otherwise MemoryError. ( 1_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv'} ), ( 1_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}), (10_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}), ( 1_000_000, \"to_tsv\", {'path': tmp/'1.tsv'}, Table.from_file, {\"path\":tmp/'1.tsv'} ), ( 1_000_000, \"to_text\", {'path': tmp/'1.txt'}, Table.from_file, {\"path\":tmp/'1.txt'} ), ( 1_000_000, \"to_html\", {'path': tmp/'1.html'}, Table.from_file, {\"path\":tmp/'1.html'} ), ( 1_000_000, \"to_hdf5\", {'path': tmp/'1.hdf5'}, Table.from_file, {\"path\":tmp/'1.hdf5'} ) ] results = Table() results.add_columns('method', 'write (s)', 'read (s)', 'rows', 'size (Mb)', 'config') results.add_rows( to_sql_benchmark(t) ) results.add_rows( to_json_benchmark(t) ) for arg in args: if len(t) In\u00a0[15]: Copied!
ieb = import_export_benchmarks(tables)\n
ieb = import_export_benchmarks(tables)
.........writing 12,000,000 records to /tmp/junk/1.hdf5... done\n
In\u00a0[16]: Copied!
ieb\n
ieb Out[16]: #methodwrite (s)read (s)rowssize (Mb)configread r/secwrite r/sec 0to_sql12.34501000000nil81004 1to_json10.8144.406100000014222696392472 2to_xlsx10.56921.5721000009{'sheet': 'pyexcel_sheet1'}46359461 3to_ods29.17529.487500003{'sheet': 'pyexcel_sheet1'}16951713 4to_csv14.31515.7311000000108{}6356869856 5to_csv14.4388.1691000000108{'guess_datatypes': False}12241469261 6to_csv140.64599.45100000001080{'guess_datatypes': False}10055371100 7to_tsv13.83415.7631000000108{}6343972285 8to_text13.93715.6821000000108{}6376771751 9to_html12.5780.531000000228{}18867927950310to_hdf55.0112.3451000000316{}81004199600

Conclusions

Best:

  • to/from JSON wins with 2.3M rps read
  • to/from CSV/TSV/TEXT comes 2nd with config guess_datatypes=False with ~ 100k rps

Worst:

  • to/from ods burst the memory footprint and hence had to be reduced to 100k rows. It also had the slowest read rate with 1450 rps.
In\u00a0[17]: Copied!
def contains_benchmark(table):\n    results = Table()\n    results.add_columns( \"column\", \"time (s)\" )\n    for name,col in table.columns.items():\n        n = len(col)\n        start,stop,step = int(n*0.02), int(n*0.98), int(n/100)\n        selection = col[start:stop:step]\n        total_time = 0.0\n        for v in selection:\n            start_time = perf_counter()\n            v in col  # <--- test!\n            end_time = perf_counter()\n            total_time += (end_time - start_time)\n        avg_time = total_time / len(selection)\n        results.add_rows( name, round(avg_time,3) )\n\n    return results\n
def contains_benchmark(table): results = Table() results.add_columns( \"column\", \"time (s)\" ) for name,col in table.columns.items(): n = len(col) start,stop,step = int(n*0.02), int(n*0.98), int(n/100) selection = col[start:stop:step] total_time = 0.0 for v in selection: start_time = perf_counter() v in col # <--- test! end_time = perf_counter() total_time += (end_time - start_time) avg_time = total_time / len(selection) results.add_rows( name, round(avg_time,3) ) return results In\u00a0[18]: Copied!
has_it = contains_benchmark(tables[-1])\nhas_it\n
has_it = contains_benchmark(tables[-1]) has_it Out[18]: #columntime (s) 0#0.001 110.043 220.032 330.001 440.001 550.001 660.006 770.003 880.006 990.00710100.04311110.655 In\u00a0[19]: Copied!
def slicing_benchmark(table):\n    n = len(table)\n    start,stop,step = int(0.02*n), int(0.98*n), int(n / 20)  # from 2% to 98% in 20 large steps\n    start_time = perf_counter()\n    snip = table[start:stop:step]\n    end_time = perf_counter()\n    print(f\"reading {len(table):,} rows to find {len(snip):,} rows took {end_time-start_time:.3f} sec\")\n    return snip\n
def slicing_benchmark(table): n = len(table) start,stop,step = int(0.02*n), int(0.98*n), int(n / 20) # from 2% to 98% in 20 large steps start_time = perf_counter() snip = table[start:stop:step] end_time = perf_counter() print(f\"reading {len(table):,} rows to find {len(snip):,} rows took {end_time-start_time:.3f} sec\") return snip In\u00a0[20]: Copied!
slice_it = slicing_benchmark(tables[-1])\n
slice_it = slicing_benchmark(tables[-1])
reading 50,000,000 rows to find 20 rows took 1.435 sec\n
In\u00a0[22]: Copied!
def column_selection_benchmark(tables):\n    results = Table()\n    results.add_columns( 'rows')\n    results.add_columns(*[f\"n cols={i}\" for i,_ in enumerate(tables[0].columns,start=1)])\n\n    for table in tables:\n        rr = [len(table)]\n        for ix, name in enumerate(table.columns):\n            cols = list(table.columns)[:ix+1]\n            start_time = perf_counter()\n            table[cols]\n            end_time = perf_counter()\n            rr.append(f\"{end_time-start_time:.5f}\")\n        results.add_rows( rr )\n    return results\n
def column_selection_benchmark(tables): results = Table() results.add_columns( 'rows') results.add_columns(*[f\"n cols={i}\" for i,_ in enumerate(tables[0].columns,start=1)]) for table in tables: rr = [len(table)] for ix, name in enumerate(table.columns): cols = list(table.columns)[:ix+1] start_time = perf_counter() table[cols] end_time = perf_counter() rr.append(f\"{end_time-start_time:.5f}\") results.add_rows( rr ) return results In\u00a0[23]: Copied!
csb = column_selection_benchmark(tables)\nprint(\"times below are are in seconds\")\ncsb\n
csb = column_selection_benchmark(tables) print(\"times below are are in seconds\") csb
times below are are in seconds\n
Out[23]: #rowsn cols=1n cols=2n cols=3n cols=4n cols=5n cols=6n cols=7n cols=8n cols=9n cols=10n cols=11n cols=12 010000000.000010.000060.000040.000040.000040.000040.000040.000040.000040.000040.000040.00004 120000000.000010.000080.000030.000030.000030.000030.000030.000030.000030.000030.000040.00004 250000000.000010.000050.000040.000040.000040.000040.000040.000040.000040.000040.000040.00004 3100000000.000020.000050.000040.000040.000040.000040.000070.000050.000050.000050.000050.00005 4200000000.000030.000060.000050.000050.000050.000050.000060.000060.000060.000060.000060.00006 5500000000.000090.000110.000100.000090.000090.000090.000090.000090.000090.000090.000100.00009 In\u00a0[33]: Copied!
def iterrows_benchmark(table):\n    results = Table()\n    results.add_columns( 'n columns', 'time (s)')\n\n    columns = ['1']\n    for column in list(table.columns):\n        columns.append(column)\n        snip = table[columns, slice(500_000,1_500_000)]\n        start_time = perf_counter()\n        counts = 0\n        for row in snip.rows:\n            counts += 1\n        end_time = perf_counter()\n        results.add_rows( len(columns), round(end_time-start_time,3))\n\n    return results\n
def iterrows_benchmark(table): results = Table() results.add_columns( 'n columns', 'time (s)') columns = ['1'] for column in list(table.columns): columns.append(column) snip = table[columns, slice(500_000,1_500_000)] start_time = perf_counter() counts = 0 for row in snip.rows: counts += 1 end_time = perf_counter() results.add_rows( len(columns), round(end_time-start_time,3)) return results In\u00a0[34]: Copied!
iterb = iterrows_benchmark(tables[-1])\niterb\n
iterb = iterrows_benchmark(tables[-1]) iterb Out[34]: #n columnstime (s) 029.951 139.816 249.859 359.93 469.985 579.942 689.958 799.867 8109.96 9119.93210129.8311139.861 In\u00a0[35]: Copied!
import matplotlib.pyplot as plt\nplt.plot(iterb['n columns'], iterb['time (s)'])\nplt.show()\n
import matplotlib.pyplot as plt plt.plot(iterb['n columns'], iterb['time (s)']) plt.show() In\u00a0[28]: Copied!
tables[-1].types()\n
tables[-1].types() Out[28]:
{'#': {int: 50000000},\n '1': {int: 50000000},\n '2': {str: 50000000},\n '3': {int: 50000000},\n '4': {int: 50000000},\n '5': {int: 50000000},\n '6': {str: 50000000},\n '7': {str: 50000000},\n '8': {str: 50000000},\n '9': {str: 50000000},\n '10': {float: 50000000},\n '11': {str: 50000000}}
In\u00a0[29]: Copied!
def dtypes_benchmark(tables):\n    dtypes_results = Table()\n    dtypes_results.add_columns(\"rows\", \"time (s)\")\n\n    for table in tables:\n        start_time = perf_counter()\n        dt = table.types()\n        end_time = perf_counter()\n        assert isinstance(dt, dict) and len(dt) != 0\n        dtypes_results.add_rows( len(table), round(end_time-start_time, 3) )\n\n    return dtypes_results\n
def dtypes_benchmark(tables): dtypes_results = Table() dtypes_results.add_columns(\"rows\", \"time (s)\") for table in tables: start_time = perf_counter() dt = table.types() end_time = perf_counter() assert isinstance(dt, dict) and len(dt) != 0 dtypes_results.add_rows( len(table), round(end_time-start_time, 3) ) return dtypes_results In\u00a0[30]: Copied!
dtype_b = dtypes_benchmark(tables)\ndtype_b\n
dtype_b = dtypes_benchmark(tables) dtype_b Out[30]: #rowstime (s) 010000000.0 120000000.0 250000000.0 3100000000.0 4200000000.0 5500000000.001 In\u00a0[31]: Copied!
def any_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n\n    for table in tables:\n        tmp = [len(table)]\n        for column in list(table.columns):\n            v = table[column][0]\n            start_time = perf_counter()\n            _ = table.any(**{column: v})\n            end_time = perf_counter()           \n            tmp.append(round(end_time-start_time,3))\n\n        results.add_rows( tmp )\n    return results\n
def any_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): v = table[column][0] start_time = perf_counter() _ = table.any(**{column: v}) end_time = perf_counter() tmp.append(round(end_time-start_time,3)) results.add_rows( tmp ) return results In\u00a0[32]: Copied!
anyb = any_benchmark(tables)\nanyb\n
anyb = any_benchmark(tables) anyb Out[32]: ~rows#1234567891011 010000000.1330.1330.1780.1330.2920.1470.1690.1430.2270.2590.1460.17 120000000.2680.2630.3430.2650.5670.2940.3350.2750.4640.5230.2890.323 250000000.6690.6530.9140.6691.4360.7230.8380.6941.1741.3350.6780.818 3100000001.3141.351.7451.3362.9021.491.6831.4142.3542.6181.3431.536 4200000002.5562.5343.3372.6025.6452.8273.2252.6464.5145.082.6933.083 5500000006.5716.4238.4556.69914.4847.9897.7986.25910.98912.486.7327.767 In\u00a0[36]: Copied!
def all_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n\n    for table in tables:\n        tmp = [len(table)]\n        for column in list(table.columns):\n            v = table[column][0]\n            start_time = perf_counter()\n            _ = table.all(**{column: v})\n            end_time = perf_counter()           \n            tmp.append(round(end_time-start_time,3))\n\n        results.add_rows( tmp )\n    return results\n
def all_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): v = table[column][0] start_time = perf_counter() _ = table.all(**{column: v}) end_time = perf_counter() tmp.append(round(end_time-start_time,3)) results.add_rows( tmp ) return results In\u00a0[37]: Copied!
allb = all_benchmark(tables)\nallb\n
allb = all_benchmark(tables) allb Out[37]: ~rows#1234567891011 010000000.120.1210.1620.1220.2640.1380.1550.1270.2090.2370.1330.151 120000000.2370.2350.3110.2380.520.2660.2970.3410.4510.530.2610.285 250000000.6750.6980.9520.5941.6050.6590.8120.7191.2241.3530.6640.914 3100000001.3141.3321.7071.3323.0911.4631.7811.3662.3582.6381.4091.714 4200000002.5762.3133.112.3965.2072.5732.9212.4034.0414.6582.4632.808 5500000005.8965.827.735.95612.9097.457.275.98110.18311.5766.3727.414 In\u00a0[\u00a0]: Copied!
\n
In\u00a0[38]: Copied!
def unique_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n        length = len(table)\n\n        tmp = [len(table)]\n        for column in list(table.columns):\n            start_time = perf_counter()\n            try:\n                L = table[column].unique()\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            assert 0 < len(L) <= length    \n\n        results.add_rows( tmp )\n    return results\n
def unique_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: length = len(table) tmp = [len(table)] for column in list(table.columns): start_time = perf_counter() try: L = table[column].unique() dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) assert 0 < len(L) <= length results.add_rows( tmp ) return results In\u00a0[39]: Copied!
ubm = unique_benchmark(tables)\nubm\n
ubm = unique_benchmark(tables) ubm Out[39]: ~rows#1234567891011 010000000.0220.0810.2480.0440.0160.0610.1150.1360.0960.0850.0940.447 120000000.1760.2710.5050.0870.0310.1240.2290.2790.1980.170.3051.471 250000000.1980.4991.2630.2180.0760.3110.570.6850.4740.4250.5952.744 3100000000.5021.1232.5350.4330.1550.6151.1281.3750.960.851.3165.826 4200000000.9562.3365.0350.8830.3191.2292.2682.7481.9131.7462.73311.883 5500000002.3956.01912.4992.1780.7643.0735.6086.8194.8284.2797.09730.511 In\u00a0[40]: Copied!
def index_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n\n        tmp = [len(table)]\n        for column in list(table.columns):\n            start_time = perf_counter()\n            try:\n                _ = table.index(column)\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            \n        results.add_rows( tmp )\n    return results\n
def index_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): start_time = perf_counter() try: _ = table.index(column) dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) results.add_rows( tmp ) return results In\u00a0[41]: Copied!
ibm = index_benchmark(tables)\nibm\n
ibm = index_benchmark(tables) ibm Out[41]: ~rows#1234567891011 010000001.9491.7931.4321.1061.0511.231.3381.4931.4111.3031.9992.325 120000002.8833.5172.8562.2172.1242.4622.6762.9862.7092.6064.0494.461 250000006.3829.0497.0965.6285.3536.3126.6497.5216.716.45910.2710.747 31000000012.55318.50613.9511.33510.72412.50913.3315.05113.50212.89919.76921.999 42000000024.71737.89628.56822.66621.47226.32727.15730.06427.33225.82238.31143.399 55000000063.01697.07772.00755.60954.09961.79768.23675.0769.02266.15299.183109.969

Multi-column index next:

In\u00a0[42]: Copied!
def multi_column_index_benchmark(tables):\n    \n    selection = [\"4\", \"7\", \"8\", \"9\"]\n    results = Table()\n    results.add_columns(\"rows\", *range(1,len(selection)+1))\n    \n    for table in tables:\n\n        tmp = [len(table)]\n        for index in range(1,5):\n            start_time = perf_counter()\n            try:\n                _ = table.index(*selection[:index])\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            print('.', end='')\n            \n        results.add_rows( tmp )\n    return results\n
def multi_column_index_benchmark(tables): selection = [\"4\", \"7\", \"8\", \"9\"] results = Table() results.add_columns(\"rows\", *range(1,len(selection)+1)) for table in tables: tmp = [len(table)] for index in range(1,5): start_time = perf_counter() try: _ = table.index(*selection[:index]) dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) print('.', end='') results.add_rows( tmp ) return results In\u00a0[43]: Copied!
mcib = multi_column_index_benchmark(tables)\nmcib\n
mcib = multi_column_index_benchmark(tables) mcib
........................
Out[43]: #rows1234 010000001.0582.1333.2154.052 120000002.124.2786.5468.328 250000005.30310.8916.69320.793 31000000010.58122.40733.46241.91 42000000021.06445.95467.78184.828 55000000052.347109.551166.6211.053 In\u00a0[44]: Copied!
def drop_duplicates_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n        result = [len(table)]\n        cols = []\n        for name in list(table.columns):\n            cols.append(name)\n            start_time = perf_counter()\n            try:\n                _ = table.drop_duplicates(*cols)\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            result.append(round(dt,3))\n            print('.', end='')\n        \n        results.add_rows( result )\n    return results\n
def drop_duplicates_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: result = [len(table)] cols = [] for name in list(table.columns): cols.append(name) start_time = perf_counter() try: _ = table.drop_duplicates(*cols) dt = perf_counter() - start_time except MemoryError: dt = -1 result.append(round(dt,3)) print('.', end='') results.add_rows( result ) return results In\u00a0[45]: Copied!
ddb = drop_duplicates_benchmark(tables)\nddb\n
ddb = drop_duplicates_benchmark(tables) ddb
........................................................................
Out[45]: ~rows#1234567891011 010000001.7612.3583.3133.9014.6154.9615.8356.5347.4548.1088.8039.682 120000003.0114.936.9347.979.26410.26812.00613.51714.9216.63117.93219.493 250000006.82713.85318.63721.23724.54827.1131.15735.02638.99243.53146.02250.433 31000000013.23831.74641.14146.91753.17258.24167.99274.65182.7491.45897.666104.82 42000000025.93277.75100.34109.314123.514131.874148.432163.57179.121196.047208.686228.059 55000000064.237312.222364.886388.249429.724466.685494.418535.367581.666607.306634.343683.858"},{"location":"benchmarks/#benchmarks","title":"Benchmarks\u00b6","text":"

These benchmarks seek to establish the performance of tablite as a user sees it.

Overview

Input/Output Various column functions Base functions Core functions - Save / Load .tpz format- Save tables to various formats- Import data from various formats - Setitem / getitem- iter- equal, not equal- copy- t += t- t *= t- contains- remove all- replace- index- unique- histogram- statistics- count - Setitem / getitem- iter / rows- equal, not equal- load- save- copy- stack- types- display_dict- show- to_dict- as_json_serializable- index - expression- filter- sort_index- reindex- drop_duplicates- sort- is_sorted- any- all- drop - replace- groupby- pivot- joins- lookup- replace missing values- transpose- pivot_transpose- diff"},{"location":"benchmarks/#input-output","title":"Input / Output\u00b6","text":""},{"location":"benchmarks/#create-tables-from-synthetic-data","title":"Create tables from synthetic data.\u00b6","text":""},{"location":"benchmarks/#save-load-tpz-format","title":"Save / Load .tpz format\u00b6","text":"

Without default compression settings (10% slower than uncompressed, 20% of uncompressed filesize)

"},{"location":"benchmarks/#save-load-tables-to-from-various-formats","title":"Save / load tables to / from various formats\u00b6","text":"

The handlers for saving / export are:

  • to_sql
  • to_json
  • to_xls
  • to_ods
  • to_csv
  • to_tsv
  • to_text
  • to_html
  • to_hdf5
"},{"location":"benchmarks/#various-column-functions","title":"Various column functions\u00b6","text":"
  • Setitem / getitem
  • iter
  • equal, not equal
  • copy
  • t += t
  • t *= t
  • contains
  • remove all
  • replace
  • index
  • unique
  • histogram
  • statistics
  • count
"},{"location":"benchmarks/#various-table-functions","title":"Various table functions\u00b6","text":""},{"location":"benchmarks/#slicing","title":"Slicing\u00b6","text":"

Slicing operations are used in many places.

"},{"location":"benchmarks/#tabletypes","title":"Table.types()\u00b6","text":"

Table.types() is implemented for near constant speed lookup.

Here is an example:

"},{"location":"benchmarks/#tableany","title":"Table.any\u00b6","text":""},{"location":"benchmarks/#tableall","title":"Table.all\u00b6","text":""},{"location":"benchmarks/#tablefilter","title":"Table.filter\u00b6","text":""},{"location":"benchmarks/#tableunique","title":"Table.unique\u00b6","text":""},{"location":"benchmarks/#tableindex","title":"Table.index\u00b6","text":"

Single column index first:

"},{"location":"benchmarks/#drop-duplicates","title":"drop duplicates\u00b6","text":""},{"location":"changelog/","title":"Changelog","text":"Version Change 2023.9.0 Adding Table.match operation. 2023.8.0 Nim backend for csv importer.Improve excel importer.Improve slicing consistency.Logical cores re-enabled on *nix based systems.Filter is now type safe.Added merge utility.Various bugfixes. 2023.6.5 Fix issues with get_headers falling back to text reading when reading 0 lines of excel, fix issue where reading excel file would ignore file count, excel file reader now has parity for linecount selection. 2023.6.4 Fix a logic bug in get_headers that caused one extra line to be returned than requested. 2023.6.3 Updated the way reference counting works. Tablite now tracks references to used pages and cleans them up based on number of references to those pages in the current process. This change allows to handle deep table clones when sending tables via processes (pickling/unpickling), whereas previous implementation would corrupt all tables using same pages due to reference counting asserting that all tables are shallow copies to the same object. 2023.6.2 Updated mplite dependency, changed to soft version requirement to prevent pipeline freezes due to small bugfixes in mplite. 2023.6.1 Major change of the backend processes. Speed up of ~6x. For more see the release notes 2022.11.19 Fixed some memory leaks. 2022.11.18 copy, filter, sort, any, all methods now properly respects the table subclass.Filter for tables with under SINGLE_PROCESSING_LIMIT rows will run on same process to reduce overhead.Errors within child processes now properly propagate to parent.Table.reset_storage(include_imports=True) now allows the user to reset the storage but exclude any imported files by setting include_imports=False during Table.reset(...).Bug: A column with 1,None,2 would be written to csv & tsv as \"1,None,2\". Now it is written \"1,,2\" where None means absent.Fix mp join producing mismatched columns lengths when different table lengths are used as an input or when join product is longer than the input table. 2022.11.17 Table.load now properly subclassess the table instead of always resulting in tablite.Table.Table.from_* methods now respect subclassess, fixed some from_* methods which were instance methods and not class methods.Fixed Table.from_dict only accepting list and tuple but not tablite.Column which is an equally valid type.Fix lookup parity in single process and multiple process outputs.Fix an issue with multiprocess lookup where no matches would throw instead of producing None.Fix an issue with filtering an empty table. 2022.11.16 Changed join to process 1M rows per task to avoid potential OOM on lower memory systems. Added mp_merge_columns to MemoryManager that merges column pages into a single column.Fix join parity in single process and multiple process outputs.Fix an issue with multiprocess join where no matches would throw instead of producing None. 2022.11.15 Bump mplite to avoid deadlock issues OS kill the process. 2022.11.14 Improve locking mechanism to allow retries when opening file as the previous solution could cause deadlocks when running multiple threads. 2022.11.13 Fix an issue with copying empty pages. 2022.11.12 Tablite now is now able to create it's own temporary directory. 2022.11.11 text_reader tqdm tracks the entire process now. text_reader properly respects free memory in *nix based systems. text_reader no longer discriminates against hyperthreaded cores. 2022.11.10 get_headers now uses plain openpyxl instead of pyexcel wrapper to speed up fetch times ~10x on certain files. 2022.11.9 get_headers can fail safe on unrecognized characters. 2022.11.8 Fix a bug with task size calculation on single core systems. 2022.11.7 Added TABLITE_TMPDIR environment variable for setting tablite work directory. Characters that fail to be read text reader due to improper encoding will be skipped. Fixed an issue where single column text files with no column delimiters would be imported as empty tables. 2022.11.6 Date inference fix 2022.11.5 Fixed negative slicing issues 2022.11.4 Transpose API changes: table.transpose(...) was renamed to table.pivot_transpose(...) new table.transpose() and table.T were added, it's functionality acts similarly to numpy.T, the column headers are used the first row in the table when transposing. 2022.11.3 Bugfix for non-ascii encoded strings during t.add_rows(...) 2022.11.2 As utf-8 is ascii compatible, the file reader utils selects utf-8 instead of ascii as a default. 2022.11.1 bugfix in datatypes.infer() where 1 was inferred as int, not float. 2022.11.0 New table features: Table.diff(other, columns=...), table.remove_duplicates_rows(), table.drop_na(*arg),table.replace(target,replacement), table.imputation(sources, targets, methods=...), table.to_pandas() and Table.from_pandas(pd.DataFrame),table.to_dict(columns, slice), Table.from_dict(),table.transpose(columns, keep, ...), New column features: Column.count(item), Column[:] is guaranteed to return a python list.Column.to_numpy(slice) returns np.ndarray. new tools library: from tablite import tools with: date_range(start,end), xround(value, multiple, up=None), and, guess as short-cut for Datatypes.guess(...). bugfixes: __eq__ was updated but missed __ne__.in operator in filter would crash if datatypes were not strings. 2022.10.11 filter now accepts any expression (str) that can be compiled by pythons compiler 2022.10.11 Bugfix for .any and .all. The code now executes much faster 2022.10.10 Bugfix for Table.import_file: import_as has been removed from keywords. 2022.10.10 All Table functions now have tqdm progressbar. 2022.10.10 More robust calculation for task size for multiprocessing. 2022.10.10 Dependency update: mplite==1.2.0 is now required. 2022.10.9 Bugfix for Table.import_file: files with duplicate header names would only have last duplicate name imported.Now the headers are made unique using name_x where x is a number. 2022.10.8 Bugfix for groupby: Where keys are empty error should have been raised.Where there are no functions, unique keypairs are returned. 2022.10.7 Bugfix for Column.statistics() for an empty column 2022.10.6 Bugfix for __setitem__: tbl['a'] = [] is now seen as tbl.add_column('a')Bugfix for __getitem__: calling a missing key raises keyerror. 2022.10.5 Bugfix for summary statistics. 2022.10.4 Bugfix for join shortcut. 2022.10.3 Bugfix for DataTypes where bool was evaluated wrongly 2022.10.0 Added ability to reindex in table.reindex(index=[0,1...,n,n-1]) 2022.9.0 Added ability to store python objects (example).Added warning when user iterates over non-rectangular dataset. 2022.8.0 Added table.export(path) which exports tablite Tables to file format given by the file extension. For example my_table.export('example.xlsx').supported formats are: json, html, xlsx, xls, csv, tsv, txt, ods and sql. 2022.7.8 Added ability to forward tqdm progressbar into Table.import_file(..., tqdm=your_tqdm), so that Jupyter notebook can use it in display-methods. 2022.7.7 Added method Table.to_sql() for export to ANSI-92 SQL enginesBugfix on to_json for timedelta. Jupyter notebook provides nice view using Table._repr_html_() JS-users can use .as_json_serializable where suitable. 2022.7.6 get_headers now takes argument (path, linecount=10) 2022.7.5 added helper Table.as_json_serializable as Jupyterkernel compat. 2022.7.4 adder helper Table.to_dict, and updated Table.to_json 2022.7.3 table.to_json now takes kwargs: row_count, columns, slice_, start_on 2022.7.2 documentation update. 2022.7.1 minor bugfix. 2022.7.0 BREAKING CHANGES- Tablite now uses HDF5 as backend. - Has multiprocessing enabled by default. - Is 20x faster. - Completely new API. 2022.6.0 DataTypes.guess([list of strings]) returns the best matching python datatype."},{"location":"tutorial/","title":"Tutorial","text":"In\u00a0[1]: Copied!
from tablite import Table\n\n## To create a tablite table is as simple as populating a dictionary:\nt = Table({'A':[1,2,3], 'B':['a','b','c']})\n
from tablite import Table ## To create a tablite table is as simple as populating a dictionary: t = Table({'A':[1,2,3], 'B':['a','b','c']}) In\u00a0[2]: Copied!
## In this notebook we can show tables in the HTML style:\nt\n
## In this notebook we can show tables in the HTML style: t Out[2]: #AB 01a 12b 23c In\u00a0[3]: Copied!
## or the ascii style:\nt.show()\n
## or the ascii style: t.show()
+==+=+=+\n|# |A|B|\n+--+-+-+\n| 0|1|a|\n| 1|2|b|\n| 2|3|c|\n+==+=+=+\n
In\u00a0[4]: Copied!
## or if you'd like to inspect the table, use:\nprint(str(t))\n
## or if you'd like to inspect the table, use: print(str(t))
Table(2 columns, 3 rows)\n
In\u00a0[5]: Copied!
## You can also add all columns at once (slower) if you prefer. \nt2 = Table(headers=('A','B'), rows=((1,'a'),(2,'b'),(3,'c')))\nassert t==t2\n
## You can also add all columns at once (slower) if you prefer. t2 = Table(headers=('A','B'), rows=((1,'a'),(2,'b'),(3,'c'))) assert t==t2 In\u00a0[6]: Copied!
## or load data:\nt3 = Table.from_file('tests/data/book1.csv')\n\n## to view any table in the notebook just let jupyter show the table. If you're using the terminal use .show(). \n## Note that show gives either first and last 7 rows or the whole table if it is less than 20 rows.\nt3\n
## or load data: t3 = Table.from_file('tests/data/book1.csv') ## to view any table in the notebook just let jupyter show the table. If you're using the terminal use .show(). ## Note that show gives either first and last 7 rows or the whole table if it is less than 20 rows. t3
Collecting tasks: 'tests/data/book1.csv'\nDumping tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 487.82it/s]\n
Out[6]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[7]: Copied!
## should you however want to select the headers instead of importing everything\n## (which maybe timeconsuming), simply use get_headers(path)\nfrom tablite.tools import get_headers\nfrom pathlib import Path\npath = Path('tests/data/book1.csv')\nsample = get_headers(path, linecount=5)\nprint(f\"sample is of type {type(sample)} and has the following entries:\")\nfor k,v in sample.items():\n    print(k)\n    if isinstance(v,list):\n        for r in sample[k]:\n            print(\"\\t\", r)\n
## should you however want to select the headers instead of importing everything ## (which maybe timeconsuming), simply use get_headers(path) from tablite.tools import get_headers from pathlib import Path path = Path('tests/data/book1.csv') sample = get_headers(path, linecount=5) print(f\"sample is of type {type(sample)} and has the following entries:\") for k,v in sample.items(): print(k) if isinstance(v,list): for r in sample[k]: print(\"\\t\", r)
sample is of type <class 'dict'> and has the following entries:\ndelimiter\nbook1.csv\n\t ['a', 'b', 'c', 'd', 'e', 'f']\n\t ['1', '0.060606061', '0.090909091', '0.121212121', '0.151515152', '0.181818182']\n\t ['2', '0.121212121', '0.242424242', '0.484848485', '0.96969697', '1.939393939']\n\t ['3', '0.242424242', '0.484848485', '0.96969697', '1.939393939', '3.878787879']\n\t ['4', '0.484848485', '0.96969697', '1.939393939', '3.878787879', '7.757575758']\n\t ['5', '0.96969697', '1.939393939', '3.878787879', '7.757575758', '15.51515152']\n
In\u00a0[8]: Copied!
## to extend a table by adding columns, use t[new] = [new values]\nt['C'] = [4,5,6]\n## but make sure the column has the same length as the rest of the table!\nt\n
## to extend a table by adding columns, use t[new] = [new values] t['C'] = [4,5,6] ## but make sure the column has the same length as the rest of the table! t Out[8]: #ABC 01a4 12b5 23c6 In\u00a0[9]: Copied!
## should you want to mix datatypes, tablite will not complain:\nfrom datetime import datetime, date,time,timedelta\nimport numpy as np\n## What you put in ...\nt4 = Table()\nt4['mixed'] = [\n    -1,0,1,  # regular integers\n    -12345678909876543211234567890987654321,  # very very large integer\n    None,np.nan,  # null values \n    \"one\", \"\",  # strings\n    True,False,  # booleans\n    float('inf'), 0.01,  # floats\n    date(2000,1,1),   # date\n    datetime(2002,2,3,23,0,4,6660),  # datetime\n    time(12,12,12),  # time\n    timedelta(days=3, seconds=5678)  # timedelta\n]\n## ... is exactly what you get out:\nt4\n
## should you want to mix datatypes, tablite will not complain: from datetime import datetime, date,time,timedelta import numpy as np ## What you put in ... t4 = Table() t4['mixed'] = [ -1,0,1, # regular integers -12345678909876543211234567890987654321, # very very large integer None,np.nan, # null values \"one\", \"\", # strings True,False, # booleans float('inf'), 0.01, # floats date(2000,1,1), # date datetime(2002,2,3,23,0,4,6660), # datetime time(12,12,12), # time timedelta(days=3, seconds=5678) # timedelta ] ## ... is exactly what you get out: t4 Out[9]: #mixed 0-1 10 21 3-12345678909876543211234567890987654321 4None 5nan 6one 7 8True 9False10inf110.01122000-01-01132002-02-03 23:00:04.0066601412:12:12153 days, 1:34:38 In\u00a0[10]: Copied!
## also if you claim the values back as a python list:\nfor item in list(t4['mixed']):\n    print(item)\n
## also if you claim the values back as a python list: for item in list(t4['mixed']): print(item)
-1\n0\n1\n-12345678909876543211234567890987654321\nNone\nnan\none\n\nTrue\nFalse\ninf\n0.01\n2000-01-01\n2002-02-03 23:00:04.006660\n12:12:12\n3 days, 1:34:38\n

The column itself (__repr__) shows us the pid, file location and the entries, so you know exactly what you're working with.

In\u00a0[11]: Copied!
t4['mixed']\n
t4['mixed'] Out[11]:
Column(/tmp/tablite-tmp/pid-54911, [-1 0 1 -12345678909876543211234567890987654321 None nan 'one' '' True\n False inf 0.01 datetime.date(2000, 1, 1)\n datetime.datetime(2002, 2, 3, 23, 0, 4, 6660) datetime.time(12, 12, 12)\n datetime.timedelta(days=3, seconds=5678)])
In\u00a0[12]: Copied!
## to view the datatypes in a column, use Column.types()\ntype_dict = t4['mixed'].types()\nfor k,v in type_dict.items():\n    print(k,v)\n
## to view the datatypes in a column, use Column.types() type_dict = t4['mixed'].types() for k,v in type_dict.items(): print(k,v)
<class 'int'> 4\n<class 'NoneType'> 1\n<class 'float'> 3\n<class 'str'> 2\n<class 'bool'> 2\n<class 'datetime.date'> 1\n<class 'datetime.datetime'> 1\n<class 'datetime.time'> 1\n<class 'datetime.timedelta'> 1\n
In\u00a0[13]: Copied!
## You may have noticed that all datatypes in t3 where identified as floats, despite their origin from a text type file.\n## This is because tablite guesses the most probable datatype using the `.guess` function on each column.\n## You can use the .guess function like this:\nfrom tablite import DataTypes\nt3['a'] = DataTypes.guess(t3['a'])\n## You can also convert the datatype using a list comprehension\nt3['b'] = [float(v) for v in t3['b']]\nt3\n
## You may have noticed that all datatypes in t3 where identified as floats, despite their origin from a text type file. ## This is because tablite guesses the most probable datatype using the `.guess` function on each column. ## You can use the .guess function like this: from tablite import DataTypes t3['a'] = DataTypes.guess(t3['a']) ## You can also convert the datatype using a list comprehension t3['b'] = [float(v) for v in t3['b']] t3 Out[13]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[14]: Copied!
t = Table()\nfor column_name in 'abcde':\n    t[column_name] =[i for i in range(5)]\n
t = Table() for column_name in 'abcde': t[column_name] =[i for i in range(5)]

(2) we want to add two new columns using the functions:

In\u00a0[15]: Copied!
def f1(a,b,c):\n    return a+b+c+1\ndef f2(b,c,d):\n    return b*c*d\n
def f1(a,b,c): return a+b+c+1 def f2(b,c,d): return b*c*d

(3) and we want to compute two new columns f and g:

In\u00a0[16]: Copied!
t.add_columns('f', 'g')\n
t.add_columns('f', 'g')

(4) we can now use the filter, to iterate over the table, and add the values to the two new columns:

In\u00a0[17]: Copied!
f,g=[],[]\nfor row in t['a', 'b', 'c', 'd'].rows:\n    a, b, c, d = row\n\n    f.append(f1(a, b, c))\n    g.append(f2(b, c, d))\nt['f'] = f\nt['g'] = g\n\nassert len(t) == 5\nassert list(t.columns) == list('abcdefg')\nt\n
f,g=[],[] for row in t['a', 'b', 'c', 'd'].rows: a, b, c, d = row f.append(f1(a, b, c)) g.append(f2(b, c, d)) t['f'] = f t['g'] = g assert len(t) == 5 assert list(t.columns) == list('abcdefg') t Out[17]: #abcdefg 00000010 11111141 22222278 3333331027 4444441364

Take note that if your dataset is assymmetric, a warning will be show:

In\u00a0[18]: Copied!
assymmetric_table = Table({'a':[1,2,3], 'b':[1,2]})\nfor row in assymmetric_table.rows:\n    print(row)\n## warning at the bottom ---v\n
assymmetric_table = Table({'a':[1,2,3], 'b':[1,2]}) for row in assymmetric_table.rows: print(row) ## warning at the bottom ---v
[1, 1]\n[2, 2]\n[3, None]\n
/home/bjorn/github/tablite/tablite/base.py:1188: UserWarning: Column b has length 2 / 3. None will appear as fill value.\n  warnings.warn(f\"Column {name} has length {len(column)} / {n_max}. None will appear as fill value.\")\n
In\u00a0[19]: Copied!
table7 = Table(columns={\n'A': [1,1,2,2,3,4],\n'B': [1,1,2,2,30,40],\n'C': [-1,-2,-3,-4,-5,-6]\n})\nindex = table7.index('A', 'B')\nfor k, v in index.items():\n    print(\"key\", k, \"indices\", v)\n
table7 = Table(columns={ 'A': [1,1,2,2,3,4], 'B': [1,1,2,2,30,40], 'C': [-1,-2,-3,-4,-5,-6] }) index = table7.index('A', 'B') for k, v in index.items(): print(\"key\", k, \"indices\", v)
key (1, 1) indices [0, 1]\nkey (2, 2) indices [2, 3]\nkey (3, 30) indices [4]\nkey (4, 40) indices [5]\n

The keys are created for each unique column-key-pair, and the value is the index where the key is found. To fetch all rows for key (2,2), we can use:

In\u00a0[20]: Copied!
for ix, row in enumerate(table7.rows):\n    if ix in index[(2,2)]:\n        print(row)\n
for ix, row in enumerate(table7.rows): if ix in index[(2,2)]: print(row)
[2, 2, -3]\n[2, 2, -4]\n
In\u00a0[21]: Copied!
## to append one table to another, use + or += \nprint('length before:', len(t3))  # length before: 45\nt5 = t3 + t3  \nprint('length after +', len(t5))  # length after + 90\nt5 += t3 \nprint('length after +=', len(t5))  # length after += 135\n## if you need a lot of numbers for a test, you can repeat a table using * and *=\nt5 *= 1_000\nprint('length after +=', len(t5))  # length after += 135000\n
## to append one table to another, use + or += print('length before:', len(t3)) # length before: 45 t5 = t3 + t3 print('length after +', len(t5)) # length after + 90 t5 += t3 print('length after +=', len(t5)) # length after += 135 ## if you need a lot of numbers for a test, you can repeat a table using * and *= t5 *= 1_000 print('length after +=', len(t5)) # length after += 135000
length before: 45\nlength after + 90\nlength after += 135\nlength after += 135000\n
In\u00a0[22]: Copied!
t5\n
t5 Out[22]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606..................... 134,9933916659267088.033318534175.066637068350.0133274000000.0266548000000.0 134,9944033318534175.066637068350.0133274000000.0266548000000.0533097000000.0 134,9954166637068350.0133274000000.0266548000000.0533097000000.01066190000000.0 134,99642133274000000.0266548000000.0533097000000.01066190000000.02132390000000.0 134,99743266548000000.0533097000000.01066190000000.02132390000000.04264770000000.0 134,99844533097000000.01066190000000.02132390000000.04264770000000.08529540000000.0 134,999451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[23]: Copied!
## if your are in doubt whether your tables will be the same you can use .stack(other)\nassert t.columns != t2.columns  # compares list of column names.\nt6 = t.stack(t2)\nt6\n
## if your are in doubt whether your tables will be the same you can use .stack(other) assert t.columns != t2.columns # compares list of column names. t6 = t.stack(t2) t6 Out[23]: #abcdefgAB 00000010NoneNone 11111141NoneNone 22222278NoneNone 3333331027NoneNone 4444441364NoneNone 5NoneNoneNoneNoneNoneNoneNone1a 6NoneNoneNoneNoneNoneNoneNone2b 7NoneNoneNoneNoneNoneNoneNone3c In\u00a0[24]: Copied!
## As you can see above, t6['C'] is padded with \"None\" where t2 was missing the columns.\n\n## if you need a more detailed view of the columns you can iterate:\nfor name in t.columns:\n    col_from_t = t[name]\n    if name in t2.columns:\n        col_from_t2 = t2[name]\n        print(name, col_from_t == col_from_t2)\n    else:\n        print(name, \"not in t2\")\n
## As you can see above, t6['C'] is padded with \"None\" where t2 was missing the columns. ## if you need a more detailed view of the columns you can iterate: for name in t.columns: col_from_t = t[name] if name in t2.columns: col_from_t2 = t2[name] print(name, col_from_t == col_from_t2) else: print(name, \"not in t2\")
a not in t2\nb not in t2\nc not in t2\nd not in t2\ne not in t2\nf not in t2\ng not in t2\n
In\u00a0[25]: Copied!
## to make a copy of a table, use table.copy()\nt3_copy = t3.copy()\n\n## you can also perform multi criteria selections using getitem [ ... ]\nt3_slice = t3['a','b','d', 5:25:5]\nt3_slice\n
## to make a copy of a table, use table.copy() t3_copy = t3.copy() ## you can also perform multi criteria selections using getitem [ ... ] t3_slice = t3['a','b','d', 5:25:5] t3_slice Out[25]: #abd 061.9393939397.757575758 11162.06060606248.2424242 2161985.9393947943.757576 32163550.06061254200.2424 In\u00a0[26]: Copied!
##deleting items also works the same way:\ndel t3_slice[1:3]  # delete row number 2 & 3 \nt3_slice\n
##deleting items also works the same way: del t3_slice[1:3] # delete row number 2 & 3 t3_slice Out[26]: #abd 061.9393939397.757575758 12163550.06061254200.2424 In\u00a0[27]: Copied!
## to wipe a table, use .clear:\nt3_slice.clear()\nt3_slice\n
## to wipe a table, use .clear: t3_slice.clear() t3_slice Out[27]: Empty Table In\u00a0[28]: Copied!
## tablite uses .npy for storage because it is fast.\n## this means you can make a table persistent using .save\nlocal_file = Path(\"local_file.tpz\")\nt5.save(local_file)\n\nold_t5 = Table.load(local_file)\nprint(\"the t5 table had\", len(old_t5), \"rows\")  # the t5 table had 135000 rows\n\ndel old_t5  # only removes the in-memory object\n\nprint(\"old_t5 still exists?\", local_file.exists())\nprint(\"path:\", local_file)\n\nimport os\nos.remove(local_file)\n
## tablite uses .npy for storage because it is fast. ## this means you can make a table persistent using .save local_file = Path(\"local_file.tpz\") t5.save(local_file) old_t5 = Table.load(local_file) print(\"the t5 table had\", len(old_t5), \"rows\") # the t5 table had 135000 rows del old_t5 # only removes the in-memory object print(\"old_t5 still exists?\", local_file.exists()) print(\"path:\", local_file) import os os.remove(local_file)
loading 'local_file.tpz' file:  55%|\u2588\u2588\u2588\u2588\u2588\u258d    | 9851/18000 [00:02<00:01, 4386.96it/s]
loading 'local_file.tpz' file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 18000/18000 [00:04<00:00, 4417.27it/s]\n
the t5 table had 135000 rows\nold_t5 still exists? True\npath: local_file.tpz\n

If you want to save a table from one session to another use save=True. This tells the garbage collector to leave the tablite Table on disk, so you can load it again without changing your code.

For example:

First time you run t = Table.import_file(....big.csv) it may take a minute or two.

If you then add t.save=True and restart python, the second time you run t = Table.import_file(....big.csv) it will take a few milliseconds instead of minutes.

In\u00a0[29]: Copied!
unfiltered = Table({'a':[1,2,3,4], 'b':[10,20,30,40]})\n
unfiltered = Table({'a':[1,2,3,4], 'b':[10,20,30,40]}) In\u00a0[30]: Copied!
true,false = unfiltered.filter(\n    [\n        {\"column1\": 'a', \"criteria\":\">=\", 'value2':3}\n    ], filter_type='all'\n)\n
true,false = unfiltered.filter( [ {\"column1\": 'a', \"criteria\":\">=\", 'value2':3} ], filter_type='all' ) In\u00a0[31]: Copied!
true\n
true Out[31]: #ab 0330 1440 In\u00a0[32]: Copied!
false.show()  # using show here to show that terminal users can have a nice view too.\n
false.show() # using show here to show that terminal users can have a nice view too.
+==+=+==+\n|# |a|b |\n+--+-+--+\n| 0|1|10|\n| 1|2|20|\n+==+=+==+\n
In\u00a0[33]: Copied!
ty = Table({'a':[1,2,3,4],'b': [10,20,30,40]})\n
ty = Table({'a':[1,2,3,4],'b': [10,20,30,40]}) In\u00a0[34]: Copied!
## typical python\nany(i > 3 for i in ty['a'])\n
## typical python any(i > 3 for i in ty['a']) Out[34]:
True
In\u00a0[35]: Copied!
## hereby you can do:\nany( ty.any(**{'a':lambda x:x>3}).rows )\n
## hereby you can do: any( ty.any(**{'a':lambda x:x>3}).rows ) Out[35]:
True
In\u00a0[36]: Copied!
## if you have multiple criteria this also works:\nall( ty.all(**{'a': lambda x:x>=2, 'b': lambda x:x<=30}).rows )\n
## if you have multiple criteria this also works: all( ty.all(**{'a': lambda x:x>=2, 'b': lambda x:x<=30}).rows ) Out[36]:
True
In\u00a0[37]: Copied!
## or this if you want to see the table.\nty.all(a=lambda x:x>2, b=lambda x:x<=30)\n
## or this if you want to see the table. ty.all(a=lambda x:x>2, b=lambda x:x<=30) Out[37]: #ab 0330 In\u00a0[38]: Copied!
## As `all` and `any` returns tables, this also means that you can chain operations:\nty.any(a=lambda x:x>2).any(b=30)\n
## As `all` and `any` returns tables, this also means that you can chain operations: ty.any(a=lambda x:x>2).any(b=30) Out[38]: #ab 0330 In\u00a0[39]: Copied!
table = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9],\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10],\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0],\n})\ntable\n
table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9], 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10], 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0], }) table Out[39]: #ABC 01100 1None1001 2810 3311 4410 5611 65100 77101 89100 In\u00a0[40]: Copied!
sort_order = {'B': False, 'C': False, 'A': False}\nassert not table.is_sorted(mapping=sort_order)\n\nsorted_table = table.sort(mapping=sort_order)\nsorted_table\n
sort_order = {'B': False, 'C': False, 'A': False} assert not table.is_sorted(mapping=sort_order) sorted_table = table.sort(mapping=sort_order) sorted_table
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2719.45it/s]\ncreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 3434.20it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 1902.47it/s]\n

Sort is reasonable effective as it uses multiprocessing above a million fields.

Hint: You can set this limit in tablite.config, like this:

In\u00a0[41]: Copied!
from tablite.config import Config\nprint(f\"multiprocessing is used above {Config.SINGLE_PROCESSING_LIMIT:,} fields\")\n
from tablite.config import Config print(f\"multiprocessing is used above {Config.SINGLE_PROCESSING_LIMIT:,} fields\")
multiprocessing is used above 1,000,000 fields\n
In\u00a0[42]: Copied!
import math\nn = math.ceil(1_000_000 / (9*3))\n\ntable = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9]*n,\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n,\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0]*n,\n})\ntable\n
import math n = math.ceil(1_000_000 / (9*3)) table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9]*n, 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n, 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0]*n, }) table Out[42]: #ABC 01100 1None1001 2810 3311 4410 5611 65100............ 333,335810 333,336311 333,337410 333,338611 333,3395100 333,3407101 333,3419100 In\u00a0[43]: Copied!
import time as cputime\nstart = cputime.time()\nsort_order = {'B': False, 'C': False, 'A': False}\nsorted_table = table.sort(mapping=sort_order)  # sorts 1M values.\nprint(\"table sorting took \", round(cputime.time() - start,3), \"secs\")\nsorted_table\n
import time as cputime start = cputime.time() sort_order = {'B': False, 'C': False, 'A': False} sorted_table = table.sort(mapping=sort_order) # sorts 1M values. print(\"table sorting took \", round(cputime.time() - start,3), \"secs\") sorted_table
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00,  4.20it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 18.17it/s]
table sorting took  0.913 secs\n
\n
In\u00a0[44]: Copied!
n = math.ceil(1_000_000 / (9*3))\n\ntable = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9]*n,\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n,\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0]*n,\n})\ntable\n
n = math.ceil(1_000_000 / (9*3)) table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9]*n, 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n, 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0]*n, }) table Out[44]: #ABC 01100 1None1001 2810 3311 4410 5611 65100............ 333,335810 333,336311 333,337410 333,338611 333,3395100 333,3407101 333,3419100 In\u00a0[45]: Copied!
from tablite import GroupBy as gb\ngrpby = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)])\ngrpby\n
from tablite import GroupBy as gb grpby = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)]) grpby
groupby: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 333342/333342 [00:00<00:00, 427322.50it/s]\n
Out[45]: #CBCount(A) 0010111114 1110037038 20174076 31174076 411037038

Here is the list of groupby functions:

class GroupBy(object):    \n    max = Max  # shortcuts to avoid having to type a long list of imports.\n    min = Min\n    sum = Sum\n    product = Product\n    first = First\n    last = Last\n    count = Count\n    count_unique = CountUnique\n    avg = Average\n    stdev = StandardDeviation\n    median = Median\n    mode = Mode\n
In\u00a0[46]: Copied!
t = Table({\n    'A':[1, 1, 2, 2, 3, 3] * 2,\n    'B':[1, 2, 3, 4, 5, 6] * 2,\n    'C':[6, 5, 4, 3, 2, 1] * 2,\n})\nt\n
t = Table({ 'A':[1, 1, 2, 2, 3, 3] * 2, 'B':[1, 2, 3, 4, 5, 6] * 2, 'C':[6, 5, 4, 3, 2, 1] * 2, }) t Out[46]: #ABC 0116 1125 2234 3243 4352 5361 6116 7125 8234 92431035211361 In\u00a0[47]: Copied!
t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False)\nt2\n
t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False) t2
pivot: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 14/14 [00:00<00:00, 3643.83it/s]\n
Out[47]: #CSum(B,A=1)Count(B,A=1)Sum(B,A=2)Count(B,A=2)Sum(B,A=3)Count(B,A=3) 0622NoneNoneNoneNone 1542NoneNoneNoneNone 24NoneNone62NoneNone 33NoneNone82NoneNone 42NoneNoneNoneNone102 51NoneNoneNoneNone122 In\u00a0[48]: Copied!
numbers = Table()\nnumbers.add_column('number', data=[      1,      2,       3,       4,   None])\nnumbers.add_column('colour', data=['black', 'blue', 'white', 'white', 'blue'])\n\nletters = Table()\nletters.add_column('letter', data=[  'a',     'b',      'c',     'd',   None])\nletters.add_column('color', data=['blue', 'white', 'orange', 'white', 'blue'])\n
numbers = Table() numbers.add_column('number', data=[ 1, 2, 3, 4, None]) numbers.add_column('colour', data=['black', 'blue', 'white', 'white', 'blue']) letters = Table() letters.add_column('letter', data=[ 'a', 'b', 'c', 'd', None]) letters.add_column('color', data=['blue', 'white', 'orange', 'white', 'blue']) In\u00a0[49]: Copied!
## left join\n## SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\nleft_join = numbers.left_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\nleft_join\n
## left join ## SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color left_join = numbers.left_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) left_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1221.94it/s]\n
Out[49]: #numberletter 01None 12a 22None 3Nonea 4NoneNone 53b 63d 74b 84d In\u00a0[50]: Copied!
## inner join\n## SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\ninner_join = numbers.inner_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\ninner_join\n
## inner join ## SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color inner_join = numbers.inner_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) inner_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1121.77it/s]\n
Out[50]: #numberletter 02a 12None 2Nonea 3NoneNone 43b 53d 64b 74d In\u00a0[51]: Copied!
# outer join\n## SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\nouter_join = numbers.outer_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\nouter_join\n
# outer join ## SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color outer_join = numbers.outer_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) outer_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1585.15it/s]\n
Out[51]: #numberletter 01None 12a 22None 3Nonea 4NoneNone 53b 63d 74b 84d 9Nonec

Q: But ...I think there's a bug in the join... A: Venn diagrams do not explain joins.

A Venn diagram is a widely-used diagram style that shows the logical relation between sets, popularised by John Venn in the 1880s. The diagrams are used to teach elementary set theory, and to illustrate simple set relationshipssource: en.wikipedia.org

Joins operate over rows and when there are duplicate rows, these will be replicated in the output. Many beginners are surprised by this, because they didn't read the SQL standard.

Q: So what do I do? A: If you want to get rid of duplicates using tablite, use the index functionality across all columns and pick the first row from each index. Here's the recipe that starts with plenty of duplicates:

In\u00a0[52]: Copied!
old_table = Table({\n'A':[1,1,1,2,2,2,3,3,3],\n'B':[1,1,4,2,2,5,3,3,6],\n})\nold_table\n
old_table = Table({ 'A':[1,1,1,2,2,2,3,3,3], 'B':[1,1,4,2,2,5,3,3,6], }) old_table Out[52]: #AB 011 111 214 322 422 525 633 733 836 In\u00a0[53]: Copied!
## CREATE TABLE OF UNIQUE ENTRIES (a.k.a. DEDUPLICATE)\nnew_table = old_table.drop_duplicates()\nnew_table\n
## CREATE TABLE OF UNIQUE ENTRIES (a.k.a. DEDUPLICATE) new_table = old_table.drop_duplicates() new_table
9it [00:00, 11329.15it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1819.26it/s]\n
Out[53]: #AB 011 114 222 325 433 536

You can also use groupby; We'll get to that in a minute.

Lookup is a special case of a search loop: Say for example you are planning a concert and want to make sure that your friends can make it home using public transport: You would have to find the first departure after the concert ends towards their home. A join would only give you a direct match on the time.

Lookup allows you \"to iterate through a list of data and find the first match given a set of criteria.\"

Here's an example:

First we have our list of friends and their stops.

In\u00a0[54]: Copied!
friends = Table({\n\"name\":['Alice', 'Betty', 'Charlie', 'Dorethy', 'Edward', 'Fred'],\n\"stop\":['Downtown-1', 'Downtown-2', 'Hillside View', 'Hillside Crescent', 'Downtown-2', 'Chicago'],\n})\nfriends\n
friends = Table({ \"name\":['Alice', 'Betty', 'Charlie', 'Dorethy', 'Edward', 'Fred'], \"stop\":['Downtown-1', 'Downtown-2', 'Hillside View', 'Hillside Crescent', 'Downtown-2', 'Chicago'], }) friends Out[54]: #namestop 0AliceDowntown-1 1BettyDowntown-2 2CharlieHillside View 3DorethyHillside Crescent 4EdwardDowntown-2 5FredChicago

Next we need a list of bus routes and their time and stops. I don't have that, so I'm making one up:

In\u00a0[55]: Copied!
import random\nrandom.seed(11)\ntable_size = 40\n\ntimes = [DataTypes.time(random.randint(21, 23), random.randint(0, 59)) for i in range(table_size)]\nstops = ['Stadium', 'Hillside', 'Hillside View', 'Hillside Crescent', 'Downtown-1', 'Downtown-2',\n            'Central station'] * 2 + [f'Random Road-{i}' for i in range(table_size)]\nroute = [random.choice([1, 2, 3]) for i in stops]\n
import random random.seed(11) table_size = 40 times = [DataTypes.time(random.randint(21, 23), random.randint(0, 59)) for i in range(table_size)] stops = ['Stadium', 'Hillside', 'Hillside View', 'Hillside Crescent', 'Downtown-1', 'Downtown-2', 'Central station'] * 2 + [f'Random Road-{i}' for i in range(table_size)] route = [random.choice([1, 2, 3]) for i in stops] In\u00a0[56]: Copied!
bus_table = Table({\n\"time\":times,\n\"stop\":stops[:table_size],\n\"route\":route[:table_size],\n})\nbus_table.sort(mapping={'time': False})\n\nprint(\"Departures from Concert Hall towards ...\")\nbus_table[0:10]\n
bus_table = Table({ \"time\":times, \"stop\":stops[:table_size], \"route\":route[:table_size], }) bus_table.sort(mapping={'time': False}) print(\"Departures from Concert Hall towards ...\") bus_table[0:10]
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 1459.90it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2421.65it/s]\n
Departures from Concert Hall towards ...\n
Out[56]: #timestoproute 021:02:00Random Road-62 121:05:00Hillside Crescent2 221:06:00Hillside1 321:25:00Random Road-241 421:29:00Random Road-161 521:32:00Random Road-211 621:33:00Random Road-121 721:36:00Random Road-233 821:38:00Central station2 921:38:00Random Road-82

Let's say the concerts ends at 21:00 and it takes a 10 minutes to get to the bus-stop. Earliest departure must then be 21:10 - goodbye hugs included.

In\u00a0[57]: Copied!
lookup_1 = friends.lookup(bus_table, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop'))\nlookup1_sorted = lookup_1.sorted(mapping={'time': False, 'name':False}, sort_mode='unix')\nlookup1_sorted\n
lookup_1 = friends.lookup(bus_table, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop')) lookup1_sorted = lookup_1.sorted(mapping={'time': False, 'name':False}, sort_mode='unix') lookup1_sorted
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 6/6 [00:00<00:00, 1513.92it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2003.65it/s]\ncreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 2589.88it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 5/5 [00:00<00:00, 2034.29it/s]\n
Out[57]: #namestoptimestop_1route 0FredChicagoNoneNoneNone 1BettyDowntown-221:51:00Downtown-21 2EdwardDowntown-221:51:00Downtown-21 3CharlieHillside View22:19:00Hillside View2 4AliceDowntown-123:12:00Downtown-13 5DorethyHillside Crescent23:54:00Hillside Crescent1

Lookup's ability to custom criteria is thereby far more versatile than SQL joins.

But with great power comes great responsibility.

In\u00a0[58]: Copied!
materials = Table({\n    'bom_id': [1, 2, 3, 4, 5, 6, 7, 8, 9], \n    'partial_of': [1, 2, 3, 4, 5, 6, 7, 4, 6], \n    'sku': ['A', 'irrelevant', 'empty carton', 'pkd carton', 'empty pallet', 'pkd pallet', 'pkd irrelevant', 'ppkd carton', 'ppkd pallet'], \n    'material_id': [None, None, None, 3, None, 5, 3, 3, 5], \n    'quantity': [10, 20, 30, 40, 50, 60, 70, 80, 90]\n})\n    # 9 is a partially packed pallet of 6\n\n## multiple values.\nlooking_for = Table({\n    'bom_id': [3,4,6], \n    'moq': [1,2,3]\n    })\n
materials = Table({ 'bom_id': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'partial_of': [1, 2, 3, 4, 5, 6, 7, 4, 6], 'sku': ['A', 'irrelevant', 'empty carton', 'pkd carton', 'empty pallet', 'pkd pallet', 'pkd irrelevant', 'ppkd carton', 'ppkd pallet'], 'material_id': [None, None, None, 3, None, 5, 3, 3, 5], 'quantity': [10, 20, 30, 40, 50, 60, 70, 80, 90] }) # 9 is a partially packed pallet of 6 ## multiple values. looking_for = Table({ 'bom_id': [3,4,6], 'moq': [1,2,3] })

Our goals is now to find the quantity from the materials table based on the items in the looking_for table.

This requires two steps:

  1. lookup
  2. filter for all by dropping items that didn't match.
In\u00a0[59]: Copied!
## step 1/2:\nproducts_lookup = materials.lookup(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"), all=False)   \nproducts_lookup\n
## step 1/2: products_lookup = materials.lookup(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"), all=False) products_lookup
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 9/9 [00:00<00:00, 3651.81it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1625.38it/s]\n
Out[59]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 011ANone10NoneNone 122irrelevantNone20NoneNone 233empty cartonNone3031 344pkd carton34042 455empty palletNone50NoneNone 566pkd pallet56063 677pkd irrelevant370NoneNone 784ppkd carton38042 896ppkd pallet59063 In\u00a0[60]: Copied!
## step 2/2:\nproducts = products_lookup.all(bom_id_1=lambda x: x is not None)\nproducts\n
## step 2/2: products = products_lookup.all(bom_id_1=lambda x: x is not None) products Out[60]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 033empty cartonNone3031 144pkd carton34042 266pkd pallet56063 384ppkd carton38042 496ppkd pallet59063

The faster way to solve this problem is to use match!

Here is the example:

In\u00a0[61]: Copied!
products_matched = materials.match(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"))\nproducts_matched\n
products_matched = materials.match(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\")) products_matched Out[61]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 033empty cartonNone3031 144pkd carton34042 266pkd pallet56063 384ppkd carton38042 496ppkd pallet59063 In\u00a0[62]: Copied!
assert products == products_matched\n
assert products == products_matched In\u00a0[63]: Copied!
from tablite import Table\nt = Table()  # create table\nt.add_columns('row','A','B','C')  # add columns\n
from tablite import Table t = Table() # create table t.add_columns('row','A','B','C') # add columns

The following examples are all valid and append the row (1,2,3) to the table.

In\u00a0[64]: Copied!
t.add_rows(1, 1, 2, 3)  # individual values\nt.add_rows([2, 1, 2, 3])  # list of values\nt.add_rows((3, 1, 2, 3))  # tuple of values\nt.add_rows(*(4, 1, 2, 3))  # unpacked tuple\nt.add_rows(row=5, A=1, B=2, C=3)   # keyword - args\nt.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})  # dict / json.\n
t.add_rows(1, 1, 2, 3) # individual values t.add_rows([2, 1, 2, 3]) # list of values t.add_rows((3, 1, 2, 3)) # tuple of values t.add_rows(*(4, 1, 2, 3)) # unpacked tuple t.add_rows(row=5, A=1, B=2, C=3) # keyword - args t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3}) # dict / json.

The following examples add two rows to the table

In\u00a0[65]: Copied!
t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))  # two (or more) tuples.\nt.add_rows([9, 1, 2, 3], [10, 4, 5, 6])  # two or more lists\nt.add_rows({'row': 11, 'A': 1, 'B': 2, 'C': 3},\n          {'row': 12, 'A': 4, 'B': 5, 'C': 6})  # two (or more) dicts as args.\nt.add_rows(*[{'row': 13, 'A': 1, 'B': 2, 'C': 3},\n            {'row': 14, 'A': 1, 'B': 2, 'C': 3}])  # list of dicts.\n
t.add_rows((7, 1, 2, 3), (8, 4, 5, 6)) # two (or more) tuples. t.add_rows([9, 1, 2, 3], [10, 4, 5, 6]) # two or more lists t.add_rows({'row': 11, 'A': 1, 'B': 2, 'C': 3}, {'row': 12, 'A': 4, 'B': 5, 'C': 6}) # two (or more) dicts as args. t.add_rows(*[{'row': 13, 'A': 1, 'B': 2, 'C': 3}, {'row': 14, 'A': 1, 'B': 2, 'C': 3}]) # list of dicts. In\u00a0[66]: Copied!
t\n
t Out[66]: #rowABC 01123 12123 23123 34123 45123 56123 67123 78456 89123 9104561011123111245612131231314123

As the row incremented from 1 in the first of these examples, and finished with row: 14, you can now see the whole table above

In\u00a0[67]: Copied!
from pathlib import Path\npath = Path('tests/data/book1.csv')\ntx = Table.from_file(path)\ntx\n
from pathlib import Path path = Path('tests/data/book1.csv') tx = Table.from_file(path) tx
Collecting tasks: 'tests/data/book1.csv'\nDumping tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 444.08it/s]\n
Out[67]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0

Note that you can also add start, limit and chunk_size to the file reader. Here's an example:

In\u00a0[68]: Copied!
path = Path('tests/data/book1.csv')\ntx2 = Table.from_file(path, start=2, limit=15)\ntx2\n
path = Path('tests/data/book1.csv') tx2 = Table.from_file(path, start=2, limit=15) tx2
Collecting tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 391.22it/s]
Dumping tasks: 'tests/data/book1.csv'\n
\n
Out[68]: #abcdef 030.2424242420.4848484850.969696971.9393939393.878787879 140.4848484850.969696971.9393939393.8787878797.757575758 250.969696971.9393939393.8787878797.75757575815.51515152 361.9393939393.8787878797.75757575815.5151515231.03030303 473.8787878797.75757575815.5151515231.0303030362.06060606 587.75757575815.5151515231.0303030362.06060606124.1212121 6915.5151515231.0303030362.06060606124.1212121248.2424242 71031.0303030362.06060606124.1212121248.2424242496.4848485 81162.06060606124.1212121248.2424242496.4848485992.969697 912124.1212121248.2424242496.4848485992.9696971985.9393941013248.2424242496.4848485992.9696971985.9393943971.8787881114496.4848485992.9696971985.9393943971.8787887943.7575761215992.9696971985.9393943971.8787887943.75757615887.5151513161985.9393943971.8787887943.75757615887.5151531775.030314173971.8787887943.75757615887.5151531775.030363550.06061

How good is the file_reader?

I've included all formats in the test suite that are publicly available from the Alan Turing institute, dateutils) and Python's csv reader.

What about MM-DD-YYYY formats? Some users from the US ask why the csv reader doesn't read the month-day-year format.

The answer is simple: It's not an iso8601 format. The US month-day-year format is a locale that may be used a lot in the US, but it isn't an international standard.

If you need to work with MM-DD-YYYY you will find that the file_reader will import the values as text (str). You can then reformat it with a custom function like:

In\u00a0[69]: Copied!
s = \"03-21-1998\"\nfrom datetime import date\nf = lambda s: date(int(s[-4:]), int(s[:2]), int(s[3:5]))\nf(s)\n
s = \"03-21-1998\" from datetime import date f = lambda s: date(int(s[-4:]), int(s[:2]), int(s[3:5])) f(s) Out[69]:
datetime.date(1998, 3, 21)
In\u00a0[70]: Copied!
from tablite.import_utils import file_readers\nfor k,v in file_readers.items():\n    print(k,v)\n
from tablite.import_utils import file_readers for k,v in file_readers.items(): print(k,v)
fods <function excel_reader at 0x7f36a3ef8c10>\njson <function excel_reader at 0x7f36a3ef8c10>\nhtml <function from_html at 0x7f36a3ef8b80>\nhdf5 <function from_hdf5 at 0x7f36a3ef8a60>\nsimple <function excel_reader at 0x7f36a3ef8c10>\nrst <function excel_reader at 0x7f36a3ef8c10>\nmediawiki <function excel_reader at 0x7f36a3ef8c10>\nxlsx <function excel_reader at 0x7f36a3ef8c10>\nxls <function excel_reader at 0x7f36a3ef8c10>\nxlsm <function excel_reader at 0x7f36a3ef8c10>\ncsv <function text_reader at 0x7f36a3ef9000>\ntsv <function text_reader at 0x7f36a3ef9000>\ntxt <function text_reader at 0x7f36a3ef9000>\nods <function ods_reader at 0x7f36a3ef8ca0>\n

(2) define your new file reader

In\u00a0[71]: Copied!
def my_magic_reader(path, **kwargs):   # define your new file reader.\n    print(\"do magic with {path}\")\n    return\n
def my_magic_reader(path, **kwargs): # define your new file reader. print(\"do magic with {path}\") return

(3) add it to the list of readers.

In\u00a0[72]: Copied!
file_readers['my_special_format'] = my_magic_reader\n
file_readers['my_special_format'] = my_magic_reader

The file_readers are all in tablite.core so if you intend to extend the readers, I recommend that you start here.

In\u00a0[73]: Copied!
file = Path('example.xlsx')\ntx2.to_xlsx(file)\nos.remove(file)\n
file = Path('example.xlsx') tx2.to_xlsx(file) os.remove(file)

In\u00a0[74]: Copied!
from tablite import Table\n\nt = Table({\n'a':[1, 2, 8, 3, 4, 6, 5, 7, 9],\n'b':[10, 100, 3, 4, 16, -1, 10, 10, 10],\n})\nt.sort(mapping={\"a\":False})\nt\n
from tablite import Table t = Table({ 'a':[1, 2, 8, 3, 4, 6, 5, 7, 9], 'b':[10, 100, 3, 4, 16, -1, 10, 10, 10], }) t.sort(mapping={\"a\":False}) t
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 1674.37it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1701.89it/s]\n
Out[74]: #ab 0110 12100 234 3416 4510 56-1 6710 783 8910 In\u00a0[75]: Copied!
%pip install matplotlib -q\n
%pip install matplotlib -q
Note: you may need to restart the kernel to use updated packages.\n
In\u00a0[76]: Copied!
import matplotlib.pyplot as plt\nplt.plot(t['a'], t['b'])\nplt.ylabel('Hello Figure')\nplt.show()\n
import matplotlib.pyplot as plt plt.plot(t['a'], t['b']) plt.ylabel('Hello Figure') plt.show() In\u00a0[77]: Copied!
## Let's monitor the memory and record the observations into a table!\nimport psutil, os, gc\nfrom time import process_time,sleep\nprocess = psutil.Process(os.getpid())\n\ndef mem_time():  # go and check taskmanagers memory usage.\n    return process.memory_info().rss, process_time()\n\ndigits = 1_000_000\n\nrecords = Table({'method':[], 'memory':[], 'time':[]})\n
## Let's monitor the memory and record the observations into a table! import psutil, os, gc from time import process_time,sleep process = psutil.Process(os.getpid()) def mem_time(): # go and check taskmanagers memory usage. return process.memory_info().rss, process_time() digits = 1_000_000 records = Table({'method':[], 'memory':[], 'time':[]})

The row based format: 1 million 10-tuples

In\u00a0[78]: Copied!
before, start = mem_time()\nL = [tuple([11 for _ in range(10)]) for _ in range(digits)]\nafter, end = mem_time()  \ndel L\ngc.collect()\n\nrecords.add_rows(*('1e6 lists w. 10 integers', after - before, round(end-start,4)))\nrecords\n
before, start = mem_time() L = [tuple([11 for _ in range(10)]) for _ in range(digits)] after, end = mem_time() del L gc.collect() records.add_rows(*('1e6 lists w. 10 integers', after - before, round(end-start,4))) records Out[78]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045

The column based format: 10 columns with 1M values:

In\u00a0[79]: Copied!
before, start = mem_time()\nL = [[11 for i2 in range(digits)] for i1 in range(10)]\nafter,end = mem_time()\n\ndel L\ngc.collect()\nrecords.add_rows(('10 lists with 1e6 integers', after - before, round(end-start,4)))\n
before, start = mem_time() L = [[11 for i2 in range(digits)] for i1 in range(10)] after,end = mem_time() del L gc.collect() records.add_rows(('10 lists with 1e6 integers', after - before, round(end-start,4)))

We've thereby saved 50 Mb by avoiding the overhead from managing 1 million lists.

Q: But why didn't I just use an array? It would have even lower memory footprint.

A: First, array's don't handle None's and we get that frequently in dirty csv data.

Second, Table needs even less memory.

Let's try with an array:

In\u00a0[80]: Copied!
import array\n\nbefore, start = mem_time()\nL = [array.array('i', [11 for _ in range(digits)]) for _ in range(10)]\nafter,end = mem_time()\n\ndel L\ngc.collect()\nrecords.add_rows(('10 lists with 1e6 integers in arrays', after - before, round(end-start,4)))\nrecords\n
import array before, start = mem_time() L = [array.array('i', [11 for _ in range(digits)]) for _ in range(10)] after,end = mem_time() del L gc.collect() records.add_rows(('10 lists with 1e6 integers in arrays', after - before, round(end-start,4))) records Out[80]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045 110 lists with 1e6 integers752762880.1906 210 lists with 1e6 integers in arrays398336000.3633

Finally let's use a tablite.Table:

In\u00a0[81]: Copied!
before,start = mem_time()\nt = Table(columns={str(i1): [11 for i2 in range(digits)] for i1 in range(10)})\nafter,end = mem_time()\n\nrecords.add_rows(('Table with 10 columns with 1e6 integers', after - before, round(end-start,4)))\n\nbefore,start = mem_time()\nt2 = t.copy()\nafter,end = mem_time()\n\nrecords.add_rows(('2 Tables with 10 columns with 1e6 integers each', after - before, round(end-start,4)))\n\n## Let's show it, so we know nobody's cheating:\nt2\n
before,start = mem_time() t = Table(columns={str(i1): [11 for i2 in range(digits)] for i1 in range(10)}) after,end = mem_time() records.add_rows(('Table with 10 columns with 1e6 integers', after - before, round(end-start,4))) before,start = mem_time() t2 = t.copy() after,end = mem_time() records.add_rows(('2 Tables with 10 columns with 1e6 integers each', after - before, round(end-start,4))) ## Let's show it, so we know nobody's cheating: t2 Out[81]: #0123456789 011111111111111111111 111111111111111111111 211111111111111111111 311111111111111111111 411111111111111111111 511111111111111111111 611111111111111111111................................. 999,99311111111111111111111 999,99411111111111111111111 999,99511111111111111111111 999,99611111111111111111111 999,99711111111111111111111 999,99811111111111111111111 999,99911111111111111111111 In\u00a0[82]: Copied!
records\n
records Out[82]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045 110 lists with 1e6 integers752762880.1906 210 lists with 1e6 integers in arrays398336000.3633 3Table with 10 columns with 1e6 integers01.9569 42 Tables with 10 columns with 1e6 integers each00.0001

Conclusion: whilst the common worst case (1M lists with 10 integers) take up 118 Mb of RAM, Tablite's tables vanish in the noise of memory measurement.

Pandas also permits the usage of namedtuples, which are unpacked upon entry.

from collections import namedtuple\nPoint = namedtuple(\"Point\", \"x y\")\npoints = [Point(0, 0), Point(0, 3)]\npd.DataFrame(points)\n

Doing that in tablite is a bit different. To unpack the named tuple, you should do so explicitly:

t = Table({'x': [p.x for p in points], 'y': [p.y for p in points]})\n

However should you want to keep the points as namedtuple, you can do so in tablite:

t = Table()\nt['points'] = points\n

Tablite will store a serialised version of the points, so your memory overhead will be close to zero.

"},{"location":"tutorial/#tablite","title":"Tablite\u00b6","text":""},{"location":"tutorial/#introduction","title":"Introduction\u00b6","text":"

Tablite fills the data-science space where incremental data processing based on:

  • Datasets are larger than memory.
  • You don't want to worry about datatypes.

Tablite thereby competes with:

  • Pandas, but saves the memory overhead.
  • Numpy, but spares you from worrying about lower level data types
  • SQlite, by sheer speed.
  • Polars, by working beyond RAM.
  • Other libraries for data cleaning thanks to tablites powerful datatypes module.

Install: pip install tablite

Usage: >>> from tablite import Table

Upgrade: pip install tablite --no-cache --upgrade

"},{"location":"tutorial/#overview","title":"Overview\u00b6","text":"

(Version 2023.6.0 and later. For older version see this)

  • Tablite handles all Python datatypes: str, float, bool, int, date, datetime, time, timedelta and None.
  • you can select:
    • all rows in a column as table['A']
    • rows across all columns as table[4:8]
    • or a slice as table['A', 'B', slice(4,8) ].
  • you to update with table['A'][2] = new value
  • you can store or send data using json, by:
    • dumping to json: json_str = table.to_json(), or
    • you can load it with Table.from_json(json_str).
  • you can iterate over rows using for row in Table.rows.
  • you can ask column_xyz in Table.colums ?
  • load from files with new_table = Table.from_file('this.csv') which has automatic datatype detection
  • perform inner, outer & left sql join between tables as simple as table_1.inner_join(table2, keys=['A', 'B'])
  • summarise using table.groupby( ... )
  • create pivot tables using groupby.pivot( ... )
  • perform multi-criteria lookup in tables using table1.lookup(table2, criteria=.....
  • and of course a large selection of tools in from tablite.tools import *
"},{"location":"tutorial/#examples","title":"Examples\u00b6","text":"

Here are some examples:

"},{"location":"tutorial/#api-examples","title":"API Examples\u00b6","text":"

In the following sections, example are given of the Tablite API's power features:

  • Iteration
  • Append
  • Sort
  • Filter
  • Index
  • Search All
  • Search Any
  • Lookup
  • Join inner, outer,
  • GroupBy
  • Pivot table
"},{"location":"tutorial/#iteration","title":"ITERATION!\u00b6","text":"

Iteration supports for loops and list comprehension at the speed of light:

Just use [r for r in table.rows], or:

for row in table.rows:\n    row ...

Here's a more practical use case:

(1) Imagine a table with columns a,b,c,d,e (all integers) like this:

"},{"location":"tutorial/#create-index-indices","title":"Create Index / Indices\u00b6","text":"

Index supports multi-key indexing using args such as: index = table.index('B','C').

Here's an example:

"},{"location":"tutorial/#append","title":"APPEND\u00b6","text":""},{"location":"tutorial/#save","title":"SAVE\u00b6","text":""},{"location":"tutorial/#filter","title":"FILTER!\u00b6","text":""},{"location":"tutorial/#any-all","title":"Any! All?\u00b6","text":"

Any and All are cousins of the filter. They're there so you can use them in the same way as you'd use any and all in python - as boolean evaluators:

"},{"location":"tutorial/#sort","title":"SORT!\u00b6","text":""},{"location":"tutorial/#groupby","title":"GROUPBY !\u00b6","text":""},{"location":"tutorial/#did-i-say-pivot-table-yes","title":"Did I say pivot table? Yes.\u00b6","text":"

Pivot Table is included in the groupby functionality - so yes - you can pivot the groupby on any column that is used for grouping. Here's a simple example:

"},{"location":"tutorial/#join","title":"JOIN!\u00b6","text":""},{"location":"tutorial/#lookup","title":"LOOKUP!\u00b6","text":""},{"location":"tutorial/#match","title":"Match\u00b6","text":"

If you're looking to do a join where you afterwards remove the empty rows, match is the faster choice.

Here is an example.

Let's start with two tables:

"},{"location":"tutorial/#are-there-other-ways-i-can-add-data","title":"Are there other ways I can add data?\u00b6","text":"

Yes - but row based operations cause a lot of IO, so it'll work but be slower:

"},{"location":"tutorial/#okay-great-how-do-i-load-data","title":"Okay, great. How do I load data?\u00b6","text":"

Easy. Use file_reader. Here's an example:

"},{"location":"tutorial/#sweet-what-formats-are-supported-can-i-add-my-own-file-reader","title":"Sweet. What formats are supported? Can I add my own file reader?\u00b6","text":"

Yes! This is very good for special log files or custom json formats. Here's how you do it:

(1) Go to all existing readers in the tablite.core and find the closest match.

"},{"location":"tutorial/#very-nice-how-about-exporting-data","title":"Very nice. How about exporting data?\u00b6","text":"

Just use .export

"},{"location":"tutorial/#cool-does-it-play-well-with-plotting-packages","title":"Cool. Does it play well with plotting packages?\u00b6","text":"

Yes. Here's an example you can copy and paste:

"},{"location":"tutorial/#i-like-sql-can-tablite-understand-sql","title":"I like sql. Can tablite understand SQL?\u00b6","text":"

Almost. You can use table.to_sql and tablite will return ANSI-92 compliant SQL.

You can also create a table using Table.from_sql and tablite will consume ANSI-92 compliant SQL.

"},{"location":"tutorial/#but-what-do-i-do-if-im-about-to-run-out-of-memory","title":"But what do I do if I'm about to run out of memory?\u00b6","text":"

You wont. Every tablite table is backed by disk. The memory footprint of a table is only the metadata required to know the relationships between variable names and the datastructures.

Let's do a comparison:

"},{"location":"tutorial/#conclusions","title":"Conclusions\u00b6","text":"

This concludes the mega-tutorial to tablite. There's nothing more to it. But oh boy it'll save a lot of time.

Here's a summary of features:

  • Everything a list can do.
  • import csv*, fods, json, html, simple, rst, mediawiki, xlsx, xls, xlsm, csv, tsv, txt, ods using Table.from_file(...)
  • Iterate over rows or columns
  • Create multikey index, sort, use filter, any and all to select. Perform lookup across tables including using custom functions.
  • Perform multikey joins with other tables.
  • Perform groupby and reorganise data as a pivot table with max, min, sum, first, last, count, unique, average, standard deviation, median and mode.
  • Update tables with += which automatically sorts out the columns - even if they're not in perfect order.
"},{"location":"tutorial/#faq","title":"FAQ\u00b6","text":"Question Answer I'm not in a notebook. Is there a nice way to view tables? Yes. table.show() prints the ascii version I'm looking for the equivalent to apply in pandas. Just use list comprehensions: table[column] = [f(x) for x in table[column] What about map? Just use the python function: mapping = map(f, table[column name]) Is there a where function? It's called any or all like in python: table.any(column_name > 0). I like sql and sqlite. Can I use sql? Yes. Call table.to_sql() returns ANSI-92 SQL compliant table definition.You can use this in any SQL compliant engine.

| sometimes i need to clean up data with datetimes. Is there any tool to help with that? | Yes. Look at DataTypes.DataTypes.round(value, multiple) allows rounding of datetime.

"},{"location":"tutorial/#coming-to-tablite-from-pandas","title":"Coming to Tablite from Pandas\u00b6","text":"

If you're coming to Tablite from Pandas you will notice some differences.

Here's the ultra short comparison to the documentation from Pandas called 10 minutes intro to pandas

The tutorials provide the generic overview:

  • pandas tutorial
  • tablite tutorial

Some key differences

topic Tablite Viewing data Just use table.show() in print outs, or if you're in a jupyter notebook just use the variable name table Selection Slicing works both on columns and rows, and you can filter using any or all:table['A','B', 2:30:3].any(A=lambda x:x>3) to copy a table use: t2 = t.copy()This is a very fast deep copy, that has no memory overhead as tablites memory manager keeps track of the data. Missing data Tablite uses mixed column format for any format that isn't uniformTo get rid of rows with Nones and np.nans use any:table.drop_na(None, np.nan) Alternatively you can use replace: table.replace(None,5) following the syntax: table.replace_missing_values(sources, target) Operations Descriptive statistics are on a colum by column basis:table['a'].statistics() the pandas function df.apply doesn't exist in tablite. Use a list comprehension instead. For example: df.apply(np.cumsum) is just np.cumsum(t['A']) \"histogramming\" in tablite is per column: table['a'].histogram() string methods? Just use a list comprehensions: table['A', 'B'].any(A=lambda x: \"hello\" in x, B=lambda x: \"world\" in x) Merge Concatenation: Just use + or += as in t1 = t2 + t3 += t4. If the columns are out of order, tablite will sort the headers according to the order in the first table.If you're worried that the header mismatch use t1.stack(t2) Joins are ANSI92 compliant: t1.join(t2, <...args...>, join_type=...). Grouping Tablite supports multikey groupby using from tablite import Groupby as gb. table.groupby(keys, functions) Reshaping To reshape a table use transpose. to perform pivot table like operations, use: table.pivot(rows, columns, functions) subtotals aside tablite will give you everything Excels pivot table can do. Time series To convert time series use a list comprehension.t1['GMT'] = [timedelta(hours=1) + v for v in t1['date'] ] to generate a date range use:from Tablite import dateranget['date'] = date_range(start=2022/1/1, stop=2023/1/1, step=timedelta(days=1)) Categorical Pandas only seems to use this for sorting and grouping. Tablite table has .sort, .groupby and .pivot to achieve the same task. Plotting Import your favorite plotting package and feed it the values, such as:import matplotlib.pyplot as plt plt.plot(t['a'],t['b']) plt.showw() Import/Export Tablite supports the same import/export options as pandas.Tablite pegs the free memory before IO and can therefore process larger-than-RAM files. Tablite also guesses the datatypes for all ISOformats and uses multiprocessing and may therefore be faster. Should you want to inspect how guess works, use from tools import guess and try the function out. Gotchas None really. Should you come across something non-pythonic, then please post it on the issue list."},{"location":"reference/_nimlite/","title":"nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite","title":"tablite._nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite-modules","title":"Modules","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite","title":"tablite._nimlite.nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-attributes","title":"Attributes","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__doc__","title":"tablite._nimlite.nimlite.__doc__ = ''","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__file__","title":"tablite._nimlite.nimlite.__file__ = '/home/runner/work/tablite/tablite/tablite/_nimlite/nimlite.so'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__name__","title":"tablite._nimlite.nimlite.__name__ = 'tablite._nimlite.nimlite'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__package__","title":"tablite._nimlite.nimlite.__package__ = 'tablite._nimlite'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-classes","title":"Classes","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.NimPyException","title":"tablite._nimlite.nimlite.NimPyException","text":"

Bases: builtins.Exception

Attributes tablite._nimlite.nimlite.NimPyException.__module__ = 'nimpy'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

tablite._nimlite.nimlite.NimPyException.__weakref__ = <attribute '__weakref__' of 'NimPyException' objects>

list of weak references to the object (if defined)

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-functions","title":"Functions","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.collect_column_select_info","title":"tablite._nimlite.nimlite.collect_column_select_info() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.do_slice_convert","title":"tablite._nimlite.nimlite.do_slice_convert() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.text_reader","title":"tablite._nimlite.nimlite.text_reader() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.text_reader_task","title":"tablite._nimlite.nimlite.text_reader_task() -> None builtin","text":""},{"location":"reference/base/","title":"Base","text":""},{"location":"reference/base/#tablite.base","title":"tablite.base","text":""},{"location":"reference/base/#tablite.base-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.log","title":"tablite.base.log = logging.getLogger(__name__) module-attribute","text":""},{"location":"reference/base/#tablite.base.file_registry","title":"tablite.base.file_registry = set() module-attribute","text":""},{"location":"reference/base/#tablite.base-classes","title":"Classes","text":""},{"location":"reference/base/#tablite.base.SimplePage","title":"tablite.base.SimplePage(id, path, len, py_dtype)","text":"

Bases: object

Source code in tablite/base.py
def __init__(self, id, path, len, py_dtype) -> None:\n    self.id = id\n    self.path = Path(path) / \"pages\" / f\"{id}.npy\"\n    self.len = len\n    self.dtype = py_dtype\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.SimplePage-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.SimplePage.ids","title":"tablite.base.SimplePage.ids = count(start=1) class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.refcounts","title":"tablite.base.SimplePage.refcounts = {} class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.autocleanup","title":"tablite.base.SimplePage.autocleanup = True class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.id","title":"tablite.base.SimplePage.id = id instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.path","title":"tablite.base.SimplePage.path = Path(path) / 'pages' / f'{id}.npy' instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.len","title":"tablite.base.SimplePage.len = len instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.dtype","title":"tablite.base.SimplePage.dtype = py_dtype instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.SimplePage.__setstate__","title":"tablite.base.SimplePage.__setstate__(state)","text":"

when an object is unpickled, say in a case of multi-processing, object.setstate(state) is called instead of init, this means we need to update page refcount as if constructor had been called

Source code in tablite/base.py
def __setstate__(self, state):\n    \"\"\"\n    when an object is unpickled, say in a case of multi-processing,\n    object.__setstate__(state) is called instead of __init__, this means\n    we need to update page refcount as if constructor had been called\n    \"\"\"\n    self.__dict__.update(state)\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.SimplePage.next_id","title":"tablite.base.SimplePage.next_id(path) classmethod","text":"Source code in tablite/base.py
@classmethod\ndef next_id(cls, path):\n    path = Path(path)\n\n    while True:\n        _id = next(cls.ids)\n        _path = path / \"pages\" / f\"{_id}.npy\"\n\n        if not _path.exists():\n            break  # make sure we don't override existing pages if they are created outside of main thread\n\n    return _id\n
"},{"location":"reference/base/#tablite.base.SimplePage.__len__","title":"tablite.base.SimplePage.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return self.len\n
"},{"location":"reference/base/#tablite.base.SimplePage.__repr__","title":"tablite.base.SimplePage.__repr__() -> str","text":"Source code in tablite/base.py
def __repr__(self) -> str:\n    try:\n        return f\"{self.__class__.__name__}({self.path}, {self.get()})\"\n    except FileNotFoundError as e:\n        return f\"{self.__class__.__name__}({self.path}, <{type(e).__name__}>)\"\n    except Exception as e:\n        return f\"{self.__class__.__name__}({self.path}, <{e}>)\"\n
"},{"location":"reference/base/#tablite.base.SimplePage.__hash__","title":"tablite.base.SimplePage.__hash__() -> int","text":"Source code in tablite/base.py
def __hash__(self) -> int:\n    return hash(self.id)\n
"},{"location":"reference/base/#tablite.base.SimplePage.owns","title":"tablite.base.SimplePage.owns()","text":"Source code in tablite/base.py
def owns(self):\n    parts = self.path.parts\n\n    return all((p in parts for p in Path(Config.pid).parts))\n
"},{"location":"reference/base/#tablite.base.SimplePage.__del__","title":"tablite.base.SimplePage.__del__()","text":"

When python's reference count for an object is 0, python uses it's garbage collector to remove the object and free the memory. As tablite tables have columns and columns have page and pages have data stored on disk, the space on disk must be freed up as well. This del override assures the cleanup of stored data.

Source code in tablite/base.py
def __del__(self):\n    \"\"\"When python's reference count for an object is 0, python uses\n    it's garbage collector to remove the object and free the memory.\n    As tablite tables have columns and columns have page and pages have\n    data stored on disk, the space on disk must be freed up as well.\n    This __del__ override assures the cleanup of stored data.\n    \"\"\"\n    if not self.owns():\n        return\n\n    refcount = self.refcounts[self.path] = max(\n        self.refcounts.get(self.path, 0) - 1, 0\n    )\n\n    if refcount > 0:\n        return\n\n    if self.autocleanup:\n        self.path.unlink(True)\n\n    del self.refcounts[self.path]\n
"},{"location":"reference/base/#tablite.base.SimplePage.get","title":"tablite.base.SimplePage.get()","text":"

loads stored data

RETURNS DESCRIPTION

np.ndarray: stored data.

Source code in tablite/base.py
def get(self):\n    \"\"\"loads stored data\n\n    Returns:\n        np.ndarray: stored data.\n    \"\"\"\n    array = load_numpy(self.path)\n    return MetaArray(array, array.dtype, py_dtype=self.dtype)\n
"},{"location":"reference/base/#tablite.base.Page","title":"tablite.base.Page(path, array)","text":"

Bases: SimplePage

PARAMETER DESCRIPTION path

working directory.

TYPE: Path

array

data

TYPE: array

Source code in tablite/base.py
def __init__(self, path, array) -> None:\n    \"\"\"\n    Args:\n        path (Path): working directory.\n        array (np.array): data\n    \"\"\"\n    _id = self.next_id(path)\n\n    type_check(array, np.ndarray)\n\n    if Config.DISK_LIMIT <= 0:\n        pass\n    else:\n        _, _, free = shutil.disk_usage(path)\n        if free - array.nbytes < Config.DISK_LIMIT:\n            msg = \"\\n\".join(\n                [\n                    f\"Disk limit reached: Config.DISK_LIMIT = {Config.DISK_LIMIT:,} bytes.\",\n                    f\"array requires {array.nbytes:,} bytes, but only {free:,} bytes are free.\",\n                    \"To disable this check, use:\",\n                    \">>> from tablite.config import Config\",\n                    \">>> Config.DISK_LIMIT = 0\",\n                    \"To free space, clean up Config.workdir:\",\n                    f\"{Config.workdir}\",\n                ]\n            )\n            raise OSError(msg)\n\n    _len = len(array)\n    # type_check(array, MetaArray)\n    if not hasattr(array, \"metadata\"):\n        raise ValueError\n    _dtype = array.metadata[\"py_dtype\"]\n\n    super().__init__(_id, path, _len, _dtype)\n\n    np.save(self.path, array, allow_pickle=True, fix_imports=False)\n    log.debug(f\"Page saved: {self.path}\")\n
"},{"location":"reference/base/#tablite.base.Page-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Page.ids","title":"tablite.base.Page.ids = count(start=1) class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.refcounts","title":"tablite.base.Page.refcounts = {} class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.autocleanup","title":"tablite.base.Page.autocleanup = True class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.id","title":"tablite.base.Page.id = id instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.path","title":"tablite.base.Page.path = Path(path) / 'pages' / f'{id}.npy' instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.len","title":"tablite.base.Page.len = len instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.dtype","title":"tablite.base.Page.dtype = py_dtype instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Page.__setstate__","title":"tablite.base.Page.__setstate__(state)","text":"

when an object is unpickled, say in a case of multi-processing, object.setstate(state) is called instead of init, this means we need to update page refcount as if constructor had been called

Source code in tablite/base.py
def __setstate__(self, state):\n    \"\"\"\n    when an object is unpickled, say in a case of multi-processing,\n    object.__setstate__(state) is called instead of __init__, this means\n    we need to update page refcount as if constructor had been called\n    \"\"\"\n    self.__dict__.update(state)\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.Page.next_id","title":"tablite.base.Page.next_id(path) classmethod","text":"Source code in tablite/base.py
@classmethod\ndef next_id(cls, path):\n    path = Path(path)\n\n    while True:\n        _id = next(cls.ids)\n        _path = path / \"pages\" / f\"{_id}.npy\"\n\n        if not _path.exists():\n            break  # make sure we don't override existing pages if they are created outside of main thread\n\n    return _id\n
"},{"location":"reference/base/#tablite.base.Page.__len__","title":"tablite.base.Page.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return self.len\n
"},{"location":"reference/base/#tablite.base.Page.__repr__","title":"tablite.base.Page.__repr__() -> str","text":"Source code in tablite/base.py
def __repr__(self) -> str:\n    try:\n        return f\"{self.__class__.__name__}({self.path}, {self.get()})\"\n    except FileNotFoundError as e:\n        return f\"{self.__class__.__name__}({self.path}, <{type(e).__name__}>)\"\n    except Exception as e:\n        return f\"{self.__class__.__name__}({self.path}, <{e}>)\"\n
"},{"location":"reference/base/#tablite.base.Page.__hash__","title":"tablite.base.Page.__hash__() -> int","text":"Source code in tablite/base.py
def __hash__(self) -> int:\n    return hash(self.id)\n
"},{"location":"reference/base/#tablite.base.Page.owns","title":"tablite.base.Page.owns()","text":"Source code in tablite/base.py
def owns(self):\n    parts = self.path.parts\n\n    return all((p in parts for p in Path(Config.pid).parts))\n
"},{"location":"reference/base/#tablite.base.Page.__del__","title":"tablite.base.Page.__del__()","text":"

When python's reference count for an object is 0, python uses it's garbage collector to remove the object and free the memory. As tablite tables have columns and columns have page and pages have data stored on disk, the space on disk must be freed up as well. This del override assures the cleanup of stored data.

Source code in tablite/base.py
def __del__(self):\n    \"\"\"When python's reference count for an object is 0, python uses\n    it's garbage collector to remove the object and free the memory.\n    As tablite tables have columns and columns have page and pages have\n    data stored on disk, the space on disk must be freed up as well.\n    This __del__ override assures the cleanup of stored data.\n    \"\"\"\n    if not self.owns():\n        return\n\n    refcount = self.refcounts[self.path] = max(\n        self.refcounts.get(self.path, 0) - 1, 0\n    )\n\n    if refcount > 0:\n        return\n\n    if self.autocleanup:\n        self.path.unlink(True)\n\n    del self.refcounts[self.path]\n
"},{"location":"reference/base/#tablite.base.Page.get","title":"tablite.base.Page.get()","text":"

loads stored data

RETURNS DESCRIPTION

np.ndarray: stored data.

Source code in tablite/base.py
def get(self):\n    \"\"\"loads stored data\n\n    Returns:\n        np.ndarray: stored data.\n    \"\"\"\n    array = load_numpy(self.path)\n    return MetaArray(array, array.dtype, py_dtype=self.dtype)\n
"},{"location":"reference/base/#tablite.base.Column","title":"tablite.base.Column(path, value=None)","text":"

Bases: object

Create Column

PARAMETER DESCRIPTION path

path of table.yml

TYPE: Path

value

Data to store. Defaults to None.

TYPE: Iterable DEFAULT: None

Source code in tablite/base.py
def __init__(self, path, value=None) -> None:\n    \"\"\"Create Column\n\n    Args:\n        path (Path): path of table.yml\n        value (Iterable, optional): Data to store. Defaults to None.\n    \"\"\"\n    self.path = path\n    self.pages = []  # keeps pointers to instances of Page\n    if value is not None:\n        self.extend(value)\n
"},{"location":"reference/base/#tablite.base.Column-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Column.path","title":"tablite.base.Column.path = path instance-attribute","text":""},{"location":"reference/base/#tablite.base.Column.pages","title":"tablite.base.Column.pages = [] instance-attribute","text":""},{"location":"reference/base/#tablite.base.Column-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Column.__len__","title":"tablite.base.Column.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return sum(len(p) for p in self.pages)\n
"},{"location":"reference/base/#tablite.base.Column.__repr__","title":"tablite.base.Column.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return f\"{self.__class__.__name__}({self.path}, {self[:]})\"\n
"},{"location":"reference/base/#tablite.base.Column.repaginate","title":"tablite.base.Column.repaginate()","text":"

resizes pages to Config.PAGE_SIZE

Source code in tablite/base.py
def repaginate(self):\n    \"\"\"resizes pages to Config.PAGE_SIZE\"\"\"\n    new_pages = []\n    start, end = 0, 0\n    for _ in range(0, len(self) + 1, Config.PAGE_SIZE):\n        start, end = end, end + Config.PAGE_SIZE\n        array = self[slice(start, end, 1)]\n\n        np_dtype, py_dtype = pytype_from_iterable(array.tolist())\n        new = MetaArray(array, dtype=np_dtype, py_dtype=py_dtype)\n\n        new_pages.append(Page(self.path, new))\n    self.pages = new_pages\n
"},{"location":"reference/base/#tablite.base.Column.extend","title":"tablite.base.Column.extend(value)","text":"

extends the column.

PARAMETER DESCRIPTION value

data

TYPE: ndarray

Source code in tablite/base.py
def extend(self, value):  # USER FUNCTION.\n    \"\"\"extends the column.\n\n    Args:\n        value (np.ndarray): data\n    \"\"\"\n    if isinstance(value, Column):\n        self.pages.extend(value.pages[:])\n        return\n    elif isinstance(value, np.ndarray):\n        pass\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n    else:\n        raise TypeError(f\"Cannot extend Column with {type(value)}\")\n    type_check(value, np.ndarray)\n    for array in self._paginate(value):\n        self.pages.append(Page(path=self.path, array=array))\n
"},{"location":"reference/base/#tablite.base.Column.clear","title":"tablite.base.Column.clear()","text":"

clears the column. Like list().clear()

Source code in tablite/base.py
def clear(self):\n    \"\"\"\n    clears the column. Like list().clear()\n    \"\"\"\n    self.pages.clear()\n
"},{"location":"reference/base/#tablite.base.Column.getpages","title":"tablite.base.Column.getpages(item)","text":"

public non-user function to identify any pages + slices of data to be retrieved given a slice (item)

PARAMETER DESCRIPTION item

target slice of data

TYPE: (int, slice)

RETURNS DESCRIPTION

list of pages/np.ndarrays.

Example: [Page(1), Page(2), np.ndarray([4,5,6], int64)] This helps, for example when creating a copy, as the copy can reference the pages 1 and 2 and only need to store the np.ndarray that is unique to it.

Source code in tablite/base.py
def getpages(self, item):\n    \"\"\"public non-user function to identify any pages + slices\n    of data to be retrieved given a slice (item)\n\n    Args:\n        item (int,slice): target slice of data\n\n    Returns:\n        list of pages/np.ndarrays.\n\n    Example: [Page(1), Page(2), np.ndarray([4,5,6], int64)]\n    This helps, for example when creating a copy, as the copy\n    can reference the pages 1 and 2 and only need to store\n    the np.ndarray that is unique to it.\n    \"\"\"\n    # internal function\n    if isinstance(item, int):\n        if item < 0:\n            item = len(self) + item\n        item = slice(item, item + 1, 1)\n\n    type_check(item, slice)\n    is_reversed = False if (item.step is None or item.step > 0) else True\n\n    length = len(self)\n    scan_item = slice(*item.indices(length))\n    range_item = range(*item.indices(length))\n\n    pages = []\n    start, end = 0, 0\n    for page in self.pages:\n        start, end = end, end + page.len\n        if is_reversed:\n            if start > scan_item.start:\n                break\n            if end < scan_item.stop:\n                continue\n        else:\n            if start > scan_item.stop:\n                break\n            if end < scan_item.start:\n                continue\n        ro = intercept(range(start, end), range_item)\n        if len(ro) == 0:\n            continue\n        elif len(ro) == page.len:  # share the whole immutable page\n            pages.append(page)\n        else:  # fetch the slice and filter it.\n            search_slice = slice(ro.start - start, ro.stop - start, ro.step)\n            np_arr = load_numpy(page.path)\n            match = np_arr[search_slice]\n            pages.append(match)\n\n    if is_reversed:\n        pages.reverse()\n        for ix, page in enumerate(pages):\n            if isinstance(page, SimplePage):\n                data = page.get()\n                pages[ix] = np.flip(data)\n            else:\n                pages[ix] = np.flip(page)\n\n    return pages\n
"},{"location":"reference/base/#tablite.base.Column.iter_by_page","title":"tablite.base.Column.iter_by_page()","text":"

iterates over the column, page by page. This method minimizes the number of reads.

RETURNS DESCRIPTION

generator of tuple: start: int end: int data: np.ndarray

Source code in tablite/base.py
def iter_by_page(self):\n    \"\"\"iterates over the column, page by page.\n    This method minimizes the number of reads.\n\n    Returns:\n        generator of tuple:\n            start: int\n            end: int\n            data: np.ndarray\n    \"\"\"\n    start, end = 0, 0\n    for page in self.pages:\n        start, end = end, end + page.len\n        yield start, end, page.get()\n
"},{"location":"reference/base/#tablite.base.Column.__getitem__","title":"tablite.base.Column.__getitem__(item)","text":"

gets numpy array.

PARAMETER DESCRIPTION item

slice of column

TYPE: int OR slice

RETURNS DESCRIPTION

np.ndarray: results as numpy array.

Remember:

>>> R = np.array([0,1,2,3,4,5])\n>>> R[3]\n3\n>>> R[3:4]\narray([3])\n
Source code in tablite/base.py
def __getitem__(self, item):  # USER FUNCTION.\n    \"\"\"gets numpy array.\n\n    Args:\n        item (int OR slice): slice of column\n\n    Returns:\n        np.ndarray: results as numpy array.\n\n    Remember:\n    ```\n    >>> R = np.array([0,1,2,3,4,5])\n    >>> R[3]\n    3\n    >>> R[3:4]\n    array([3])\n    ```\n    \"\"\"\n    result = []\n    for element in self.getpages(item):\n        if isinstance(element, SimplePage):\n            result.append(element.get())\n        else:\n            result.append(element)\n\n    if result:\n        arr = np_type_unify(result)\n    else:\n        arr = np.array([])\n\n    if isinstance(item, int):\n        if len(arr) == 0:\n            raise IndexError(\n                f\"index {item} is out of bounds for axis 0 with size {len(self)}\"\n            )\n        return numpy_to_python(arr[0])\n    else:\n        return arr\n
"},{"location":"reference/base/#tablite.base.Column.__setitem__","title":"tablite.base.Column.__setitem__(key, value)","text":"

sets values.

PARAMETER DESCRIPTION key

selector

TYPE: (int, slice)

value

values to insert

TYPE: any

RAISES DESCRIPTION KeyError

Following normal slicing rules

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION.\n    \"\"\"sets values.\n\n    Args:\n        key (int,slice): selector\n        value (any): values to insert\n\n    Raises:\n        KeyError: Following normal slicing rules\n    \"\"\"\n    if isinstance(key, int):\n        self._setitem_integer_key(key, value)\n\n    elif isinstance(key, slice):\n        if not isinstance(value, np.ndarray):\n            value = list_to_np_array(value)\n        type_check(value, np.ndarray)\n\n        if key.start is None and key.stop is None and key.step in (None, 1):\n            self._setitem_replace_all(key, value)\n        elif key.start is not None and key.stop is None and key.step in (None, 1):\n            self._setitem_extend(key, value)\n        elif key.stop is not None and key.start is None and key.step in (None, 1):\n            self._setitem_prextend(key, value)\n        elif (\n            key.step in (None, 1) and key.start is not None and key.stop is not None\n        ):\n            self._setitem_insert(key, value)\n        elif key.step not in (None, 1):\n            self._setitem_update(key, value)\n        else:\n            raise KeyError(f\"bad key: {key}\")\n    else:\n        raise KeyError(f\"bad key: {key}\")\n
"},{"location":"reference/base/#tablite.base.Column.__delitem__","title":"tablite.base.Column.__delitem__(key)","text":"

deletes items selected by key

PARAMETER DESCRIPTION key

selector

TYPE: (int, slice)

RAISES DESCRIPTION KeyError

following normal slicing rules.

Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION\n    \"\"\"deletes items selected by key\n\n    Args:\n        key (int,slice): selector\n\n    Raises:\n        KeyError: following normal slicing rules.\n    \"\"\"\n    if isinstance(key, int):\n        self._del_by_int(key)\n    elif isinstance(key, slice):\n        self._del_by_slice(key)\n    else:\n        raise KeyError(f\"bad key: {key}\")\n
"},{"location":"reference/base/#tablite.base.Column.get_by_indices","title":"tablite.base.Column.get_by_indices(indices)","text":"

retrieves values from column given a set of indices.

PARAMETER DESCRIPTION indices

targets

TYPE: array

This method uses np.take, is faster than iterating over rows. Examples:

>>> indices = np.array(list(range(3,700_700, 426)))\n>>> arr = np.array(list(range(2_000_000)))\nPythonic:\n>>> [v for i,v in enumerate(arr) if i in indices]\nNumpyionic:\n>>> np.take(arr, indices)\n
Source code in tablite/base.py
def get_by_indices(self, indices):\n    \"\"\"retrieves values from column given a set of indices.\n\n    Args:\n        indices (np.array): targets\n\n    This method uses np.take, is faster than iterating over rows.\n    Examples:\n    ```\n    >>> indices = np.array(list(range(3,700_700, 426)))\n    >>> arr = np.array(list(range(2_000_000)))\n    Pythonic:\n    >>> [v for i,v in enumerate(arr) if i in indices]\n    Numpyionic:\n    >>> np.take(arr, indices)\n    ```\n    \"\"\"\n    type_check(indices, np.ndarray)\n\n    dtypes = set()\n    values = np.empty(\n        indices.shape, dtype=object\n    )  # placeholder for the indexed values.\n\n    for start, end, data in self.iter_by_page():\n        range_match = np.asarray(\n            ((indices >= start) & (indices < end)) | (indices == -1)\n        ).nonzero()[0]\n        if len(range_match):\n            sub_index = np.take(indices, range_match)\n            sub_index2 = np.where(sub_index == -1, -1, sub_index - start)\n            # diss: the line above is required to cover for cases where len(data) > (-1 - start)\n            #       as sub_index2 otherwise will raise index error\n            arr = np.take(data, sub_index2)\n            dtypes.add(arr.dtype)\n            np.put(values, range_match, arr)\n\n    if len(dtypes) == 1:  # simplify the datatype.\n        dtype = next(iter(dtypes))\n        values = np.array(values, dtype=dtype)\n    return values\n
"},{"location":"reference/base/#tablite.base.Column.__iter__","title":"tablite.base.Column.__iter__()","text":"Source code in tablite/base.py
def __iter__(self):  # USER FUNCTION.\n    for page in self.pages:\n        data = page.get()\n        for value in data:\n            yield value\n
"},{"location":"reference/base/#tablite.base.Column.__eq__","title":"tablite.base.Column.__eq__(other)","text":"

compares two columns. Like list1 == list2

Source code in tablite/base.py
def __eq__(self, other):  # USER FUNCTION.\n    \"\"\"\n    compares two columns. Like `list1 == list2`\n    \"\"\"\n    if len(self) != len(other):  # quick cheap check.\n        return False\n\n    if isinstance(other, (list, tuple)):\n        return all(a == b for a, b in zip(self[:], other))\n\n    elif isinstance(other, Column):\n        if self.pages == other.pages:  # special case.\n            return True\n\n        # are the pages of same size?\n        if len(self.pages) == len(other.pages):\n            if [p.len for p in self.pages] == [p.len for p in other.pages]:\n                for a, b in zip(self.pages, other.pages):\n                    if not (a.get() == b.get()).all():\n                        return False\n                return True\n        # to bad. Element comparison it is then:\n        for a, b in zip(iter(self), iter(other)):\n            if a != b:\n                return False\n        return True\n\n    elif isinstance(other, np.ndarray):\n        start, end = 0, 0\n        for p in self.pages:\n            start, end = end, end + p.len\n            if not (p.get() == other[start:end]).all():\n                return False\n        return True\n    else:\n        raise TypeError(f\"Cannot compare {self.__class__} with {type(other)}\")\n
"},{"location":"reference/base/#tablite.base.Column.__ne__","title":"tablite.base.Column.__ne__(other)","text":"

compares two columns. Like list1 != list2

Source code in tablite/base.py
def __ne__(self, other):  # USER FUNCTION\n    \"\"\"\n    compares two columns. Like `list1 != list2`\n    \"\"\"\n    if len(self) != len(other):  # quick cheap check.\n        return True\n\n    if isinstance(other, (list, tuple)):\n        return any(a != b for a, b in zip(self[:], other))\n\n    elif isinstance(other, Column):\n        if self.pages == other.pages:  # special case.\n            return False\n\n        # are the pages of same size?\n        if len(self.pages) == len(other.pages):\n            if [p.len for p in self.pages] == [p.len for p in other.pages]:\n                for a, b in zip(self.pages, other.pages):\n                    if not (a.get() == b.get()).all():\n                        return True\n                return False\n        # to bad. Element comparison it is then:\n        for a, b in zip(iter(self), iter(other)):\n            if a != b:\n                return True\n        return False\n\n    elif isinstance(other, np.ndarray):\n        start, end = 0, 0\n        for p in self.pages:\n            start, end = end, end + p.len\n            if (p.get() != other[start:end]).any():\n                return True\n        return False\n    else:\n        raise TypeError(f\"Cannot compare {self.__class__} with {type(other)}\")\n
"},{"location":"reference/base/#tablite.base.Column.copy","title":"tablite.base.Column.copy()","text":"

returns deep=copy of Column

RETURNS DESCRIPTION

Column

Source code in tablite/base.py
def copy(self):\n    \"\"\"returns deep=copy of Column\n\n    Returns:\n        Column\n    \"\"\"\n    cp = Column(path=self.path)\n    cp.pages = self.pages[:]\n    return cp\n
"},{"location":"reference/base/#tablite.base.Column.__copy__","title":"tablite.base.Column.__copy__()","text":"

see copy

Source code in tablite/base.py
def __copy__(self):\n    \"\"\"see copy\"\"\"\n    return self.copy()\n
"},{"location":"reference/base/#tablite.base.Column.__imul__","title":"tablite.base.Column.__imul__(other)","text":"

Repeats instance of column N times. Like list() * N

Example:

>>> one = Column(data=[1,2])\n>>> one *= 5\n>>> one\n[1,2, 1,2, 1,2, 1,2, 1,2]\n
Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"\n    Repeats instance of column N times. Like list() * N\n\n    Example:\n    ```\n    >>> one = Column(data=[1,2])\n    >>> one *= 5\n    >>> one\n    [1,2, 1,2, 1,2, 1,2, 1,2]\n    ```\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a column can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    self.pages = self.pages[:] * other\n    return self\n
"},{"location":"reference/base/#tablite.base.Column.__mul__","title":"tablite.base.Column.__mul__(other)","text":"

Repeats instance of column N times. Like list() * N

Example:

>>> one = Column(data=[1,2])\n>>> two = one * 5\n>>> two\n[1,2, 1,2, 1,2, 1,2, 1,2]\n
Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"\n    Repeats instance of column N times. Like list() * N\n\n    Example:\n    ```\n    >>> one = Column(data=[1,2])\n    >>> two = one * 5\n    >>> two\n    [1,2, 1,2, 1,2, 1,2, 1,2]\n    ```\n    \"\"\"\n    if not isinstance(other, int):\n        raise TypeError(\n            f\"a column can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    cp = self.copy()\n    cp *= other\n    return cp\n
"},{"location":"reference/base/#tablite.base.Column.__iadd__","title":"tablite.base.Column.__iadd__(other)","text":"Source code in tablite/base.py
def __iadd__(self, other):\n    if isinstance(other, (list, tuple)):\n        other = list_to_np_array(other)\n        self.extend(other)\n    elif isinstance(other, Column):\n        self.pages.extend(other.pages[:])\n    else:\n        raise TypeError(f\"{type(other)} not supported.\")\n    return self\n
"},{"location":"reference/base/#tablite.base.Column.__contains__","title":"tablite.base.Column.__contains__(item)","text":"

determines if item is in the Column. Similar to 'x' in ['a','b','c'] returns boolean

PARAMETER DESCRIPTION item

value to search for

TYPE: any

RETURNS DESCRIPTION bool

True if item exists in column.

Source code in tablite/base.py
def __contains__(self, item):\n    \"\"\"determines if item is in the Column.\n    Similar to `'x' in ['a','b','c']`\n    returns boolean\n\n    Args:\n        item (any): value to search for\n\n    Returns:\n        bool: True if item exists in column.\n    \"\"\"\n    for page in set(self.pages):\n        if item in page.get():  # x in np.ndarray([...]) uses np.any(arr, value)\n            return True\n    return False\n
"},{"location":"reference/base/#tablite.base.Column.remove_all","title":"tablite.base.Column.remove_all(*values)","text":"

removes all values of values

Source code in tablite/base.py
def remove_all(self, *values):\n    \"\"\"\n    removes all values of `values`\n    \"\"\"\n    type_check(values, tuple)\n    if isinstance(values[0], tuple):\n        values = values[0]\n    to_remove = list_to_np_array(values)\n    for index, page in enumerate(self.pages):\n        data = page.get()\n        bitmask = np.isin(data, to_remove)  # identify elements to remove.\n        if bitmask.any():\n            bitmask = np.invert(bitmask)  # turn bitmask around to keep.\n            new_data = np.compress(bitmask, data)\n            new_page = Page(self.path, new_data)\n            self.pages[index] = new_page\n
"},{"location":"reference/base/#tablite.base.Column.replace","title":"tablite.base.Column.replace(mapping)","text":"

replaces values using a mapping.

PARAMETER DESCRIPTION mapping

{value to replace: new value, ...}

TYPE: dict

Example:

>>> t = Table(columns={'A': [1,2,3,4]})\n>>> t['A'].replace({2:20,4:40})\n>>> t[:]\nnp.ndarray([1,20,3,40])\n
Source code in tablite/base.py
def replace(self, mapping):\n    \"\"\"\n    replaces values using a mapping.\n\n    Args:\n        mapping (dict): {value to replace: new value, ...}\n\n    Example:\n    ```\n    >>> t = Table(columns={'A': [1,2,3,4]})\n    >>> t['A'].replace({2:20,4:40})\n    >>> t[:]\n    np.ndarray([1,20,3,40])\n    ```\n    \"\"\"\n    type_check(mapping, dict)\n    to_replace = np.array(list(mapping.keys()))\n    for index, page in enumerate(self.pages):\n        data = page.get()\n        bitmask = np.isin(data, to_replace)  # identify elements to replace.\n        if bitmask.any():\n            warray = np.compress(bitmask, data)\n            for ix, v in enumerate(warray):\n                warray[ix] = mapping[numpy_to_python(v)]\n            data[bitmask] = warray\n            self.pages[index] = Page(path=self.path, array=data)\n
"},{"location":"reference/base/#tablite.base.Column.types","title":"tablite.base.Column.types()","text":"

returns dict with python datatypes

RETURNS DESCRIPTION dict

frequency of occurrence of python datatypes

Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns dict with python datatypes\n\n    Returns:\n        dict: frequency of occurrence of python datatypes\n    \"\"\"\n    d = Counter()\n    for page in self.pages:\n        assert isinstance(page.dtype, dict)\n        d += page.dtype\n    return dict(d)\n
"},{"location":"reference/base/#tablite.base.Column.index","title":"tablite.base.Column.index()","text":"

returns dict with { unique entry : list of indices }

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.index()\n{'a':[0,2], 'b': [1,4], 'c': [3]}\n
Source code in tablite/base.py
def index(self):\n    \"\"\"\n    returns dict with { unique entry : list of indices }\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.index()\n    {'a':[0,2], 'b': [1,4], 'c': [3]}\n    ```\n    \"\"\"\n    d = defaultdict(list)\n    for ix, v in enumerate(self.__iter__()):\n        d[v].append(ix)\n    return dict(d)\n
"},{"location":"reference/base/#tablite.base.Column.unique","title":"tablite.base.Column.unique()","text":"

returns unique list of values.

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.unqiue()\n['a','b','c']\n
Source code in tablite/base.py
def unique(self):\n    \"\"\"\n    returns unique list of values.\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.unqiue()\n    ['a','b','c']\n    ```\n    \"\"\"\n    arrays = []\n    for page in set(self.pages):\n        try:  # when it works, numpy is fast...\n            arrays.append(np.unique(page.get()))\n        except TypeError:  # ...but np.unique cannot handle Nones.\n            arrays.append(multitype_set(page.get()))\n    union = np_type_unify(arrays)\n    try:\n        return np.unique(union)\n    except MemoryError:\n        return np.array(set(union))\n    except TypeError:\n        return multitype_set(union)\n
"},{"location":"reference/base/#tablite.base.Column.histogram","title":"tablite.base.Column.histogram()","text":"

returns 2 arrays: unique elements and count of each element

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.histogram()\n{'a':2,'b':2,'c':1}\n
Source code in tablite/base.py
def histogram(self):\n    \"\"\"\n    returns 2 arrays: unique elements and count of each element\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.histogram()\n    {'a':2,'b':2,'c':1}\n    ```\n    \"\"\"\n    d = defaultdict(int)\n    for page in self.pages:\n        try:\n            uarray, carray = np.unique(page.get(), return_counts=True)\n        except TypeError:\n            uarray = page.get()\n            carray = repeat(1, len(uarray))\n\n        for i, c in zip(uarray, carray):\n            v = numpy_to_python(i)\n            d[(type(v), v)] += numpy_to_python(c)\n    u = [v for _, v in d.keys()]\n    c = list(d.values())\n    return u, c  # unique, counts\n
"},{"location":"reference/base/#tablite.base.Column.statistics","title":"tablite.base.Column.statistics()","text":"

provides summary statistics.

RETURNS DESCRIPTION dict

returns dict with:

  • min (int/float, length of str, date)
  • max (int/float, length of str, date)
  • mean (int/float, length of str, date)
  • median (int/float, length of str, date)
  • stdev (int/float, length of str, date)
  • mode (int/float, length of str, date)
  • distinct (int/float, length of str, date)
  • iqr (int/float, length of str, date)
  • sum (int/float, length of str, date)
  • histogram (see .histogram)
Source code in tablite/base.py
def statistics(self):\n    \"\"\"provides summary statistics.\n\n    Returns:\n        dict: returns dict with:\n        - min (int/float, length of str, date)\n        - max (int/float, length of str, date)\n        - mean (int/float, length of str, date)\n        - median (int/float, length of str, date)\n        - stdev (int/float, length of str, date)\n        - mode (int/float, length of str, date)\n        - distinct (int/float, length of str, date)\n        - iqr (int/float, length of str, date)\n        - sum (int/float, length of str, date)\n        - histogram (see .histogram)\n    \"\"\"\n    values, counts = self.histogram()\n    return summary_statistics(values, counts)\n
"},{"location":"reference/base/#tablite.base.Column.count","title":"tablite.base.Column.count(item)","text":"

counts appearances of item in column.

Note that in python, True == 1 and False == 0, whereby the following difference occurs:

in python:

>>> L = [1, True]\n>>> L.count(True)\n2\n

in tablite:

>>> t = Table({'L': [1,True]})\n>>> t['L'].count(True)\n1\n
PARAMETER DESCRIPTION item

target item

TYPE: Any

RETURNS DESCRIPTION int

number of occurrences of item.

Source code in tablite/base.py
def count(self, item):\n    \"\"\"counts appearances of item in column.\n\n    Note that in python, `True == 1` and `False == 0`,\n    whereby the following difference occurs:\n\n    in python:\n    ```\n    >>> L = [1, True]\n    >>> L.count(True)\n    2\n    ```\n    in tablite:\n    ```\n    >>> t = Table({'L': [1,True]})\n    >>> t['L'].count(True)\n    1\n    ```\n\n    Args:\n        item (Any): target item\n\n    Returns:\n        int: number of occurrences of item.\n    \"\"\"\n    result = 0\n    for page in self.pages:\n        data = page.get()\n        if data.dtype != \"O\":\n            result += np.nonzero(page.get() == item)[0].shape[0]\n            # what happens here ---^ below:\n            # arr = page.get()\n            # >>> arr\n            # array([1,2,3,4,3], int64)\n            # >>> (arr == 3)\n            # array([False, False,  True, False,  True])\n            # >>> np.nonzero(arr==3)\n            # (array([2,4], dtype=int64), )  <-- tuple!\n            # >>> np.nonzero(page.get() == item)[0]\n            # array([2,4])\n            # >>> np.nonzero(page.get() == item)[0].shape\n            # (2, )\n            # >>> np.nonzero(page.get() == item)[0].shape[0]\n            # 2\n        else:\n            result += sum(1 for i in data if type(i) == type(item) and i == item)\n    return result\n
"},{"location":"reference/base/#tablite.base.Table","title":"tablite.base.Table(columns=None, headers=None, rows=None, _path=None)","text":"

Bases: object

creates Table

PARAMETER DESCRIPTION EITHER

columns (dict, optional): dict with column names as keys, values as lists. Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})

Source code in tablite/base.py
def __init__(self, columns=None, headers=None, rows=None, _path=None) -> None:\n    \"\"\"creates Table\n\n    Args:\n        EITHER:\n            columns (dict, optional): dict with column names as keys, values as lists.\n            Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})\n        OR\n            headers (list of strings, optional): list of column names.\n            rows (list of tuples or lists, optional): values for columns\n            Example: t = Table(headers=[\"a\", \"b\"], rows=[[1,3], [2,4]])\n    \"\"\"\n    if _path is None:\n        if self._pid_dir is None:\n            self._pid_dir = Path(Config.workdir) / Config.pid\n            if not self._pid_dir.exists():\n                self._pid_dir.mkdir()\n                (self._pid_dir / \"pages\").mkdir()\n            register(self._pid_dir)\n\n        _path = Path(self._pid_dir)\n        # if path exists under the given PID it will be overwritten.\n        # this can only happen if the process previously was SIGKILLed.\n    type_check(_path, Path)\n    self.path = _path  # filename used during multiprocessing.\n    self.columns = {}  # maps colunn names to instances of Column.\n\n    # user friendly features.\n    if columns and any((headers, rows)):\n        raise ValueError(\"Either columns as dict OR headers and rows. Not both.\")\n\n    if headers and rows:\n        rotated = list(zip(*rows))\n        columns = {k: v for k, v in zip(headers, rotated)}\n\n    if columns:\n        type_check(columns, dict)\n        for k, v in columns.items():\n            self.__setitem__(k, v)\n
"},{"location":"reference/base/#tablite.base.Table-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Table.path","title":"tablite.base.Table.path = _path instance-attribute","text":""},{"location":"reference/base/#tablite.base.Table.columns","title":"tablite.base.Table.columns = {} instance-attribute","text":""},{"location":"reference/base/#tablite.base.Table.rows","title":"tablite.base.Table.rows property","text":"

enables row based iteration in python types.

Example:

for row in Table.rows:\n    print(row)\n

Yields: tuple: values is same order as columns.

"},{"location":"reference/base/#tablite.base.Table-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Table.__str__","title":"tablite.base.Table.__str__()","text":"Source code in tablite/base.py
def __str__(self):  # USER FUNCTION.\n    return f\"{self.__class__.__name__}({len(self.columns):,} columns, {len(self):,} rows)\"\n
"},{"location":"reference/base/#tablite.base.Table.__repr__","title":"tablite.base.Table.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return self.__str__()\n
"},{"location":"reference/base/#tablite.base.Table.nbytes","title":"tablite.base.Table.nbytes()","text":"

finds the total bytes of the table on disk

RETURNS DESCRIPTION tuple

int: real bytes used on disk int: total bytes used if flattened

Source code in tablite/base.py
def nbytes(self):  # USER FUNCTION.\n    \"\"\"finds the total bytes of the table on disk\n\n    Returns:\n        tuple:\n            int: real bytes used on disk\n            int: total bytes used if flattened\n    \"\"\"\n    real = {}\n    total = 0\n    for column in self.columns.values():\n        for page in set(column.pages):\n            real[page] = page.path.stat().st_size\n        for page in column.pages:\n            total += real[page]\n    return sum(real.values()), total\n
"},{"location":"reference/base/#tablite.base.Table.items","title":"tablite.base.Table.items()","text":"

returns table as dict

RETURNS DESCRIPTION dict

Table as dict {column_name: [values], ...}

Source code in tablite/base.py
def items(self):  # USER FUNCTION.\n    \"\"\"returns table as dict\n\n    Returns:\n        dict: Table as dict `{column_name: [values], ...}`\n    \"\"\"\n    return {\n        name: column[:].tolist() for name, column in self.columns.items()\n    }.items()\n
"},{"location":"reference/base/#tablite.base.Table.__delitem__","title":"tablite.base.Table.__delitem__(key)","text":"

Examples:

>>> del table['a']  # removes column 'a'\n>>> del table[-3:]  # removes last 3 rows from all columns.\n
Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION.\n    \"\"\"\n    Examples:\n    ```\n    >>> del table['a']  # removes column 'a'\n    >>> del table[-3:]  # removes last 3 rows from all columns.\n    ```\n    \"\"\"\n    if isinstance(key, (int, slice)):\n        for column in self.columns.values():\n            del column[key]\n    elif key in self.columns:\n        del self.columns[key]\n    else:\n        raise KeyError(f\"Key not found: {key}\")\n
"},{"location":"reference/base/#tablite.base.Table.__setitem__","title":"tablite.base.Table.__setitem__(key, value)","text":"

table behaves like a dict. Args: key (str or hashable): column name value (iterable): list, tuple or nd.array with values.

As Table now accepts the keyword columns as a dict:

>>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n

and the header/data combinations:

>>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n

This has the side-benefit that tuples now can be used as headers.

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION\n    \"\"\"table behaves like a dict.\n    Args:\n        key (str or hashable): column name\n        value (iterable): list, tuple or nd.array with values.\n\n    As Table now accepts the keyword `columns` as a dict:\n    ```\n    >>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n    ```\n    and the header/data combinations:\n    ```\n    >>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n    ```\n    This has the side-benefit that tuples now can be used as headers.\n    \"\"\"\n    if value is None:\n        self.columns[key] = Column(self.path, value=None)\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, (np.ndarray)):\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, Column):\n        self.columns[key] = value\n    else:\n        raise TypeError(f\"{type(value)} not supported.\")\n
"},{"location":"reference/base/#tablite.base.Table.__getitem__","title":"tablite.base.Table.__getitem__(keys)","text":"

Enables selection of columns and rows

PARAMETER DESCRIPTION keys

TYPE: column name, integer or slice

Examples

>>>

10] selects first 10 rows from all columns

TYPE: table[

>>>

20:3] selects column 'b' and 'c' and 'a' twice for a slice.

TYPE: table['b', 'a', 'a', 'c', 2

Raises: KeyError: if key is not found. TypeError: if key is not a string, integer or slice.

RETURNS DESCRIPTION Table

returns columns in same order as selection.

Source code in tablite/base.py
def __getitem__(self, keys):  # USER FUNCTION\n    \"\"\"\n    Enables selection of columns and rows\n\n    Args:\n        keys (column name, integer or slice):\n        Examples:\n        ```\n        >>> table['a']                        selects column 'a'\n        >>> table[3]                          selects row 3 as a tuple.\n        >>> table[:10]                        selects first 10 rows from all columns\n        >>> table['a','b', slice(3,20,2)]     selects a slice from columns 'a' and 'b'\n        >>> table['b', 'a', 'a', 'c', 2:20:3] selects column 'b' and 'c' and 'a' twice for a slice.\n        >>> table[('b', 'a', 'a', 'c')]       selects columns 'b', 'a', 'a', and 'c' using a tuple.\n        ```\n    Raises:\n        KeyError: if key is not found.\n        TypeError: if key is not a string, integer or slice.\n\n    Returns:\n        Table: returns columns in same order as selection.\n    \"\"\"\n\n    if not isinstance(keys, tuple):\n        if isinstance(keys, list):\n            keys = tuple(keys)\n        else:\n            keys = (keys,)\n    if isinstance(keys[0], tuple):\n        keys = tuple(list(chain(*keys)))\n\n    integers = [i for i in keys if isinstance(i, int)]\n    if len(integers) == len(keys) == 1:  # return a single tuple.\n        keys = [slice(keys[0])]\n\n    column_names = [i for i in keys if isinstance(i, str)]\n    column_names = list(self.columns) if not column_names else column_names\n    not_found = [name for name in column_names if name not in self.columns]\n    if not_found:\n        raise KeyError(f\"keys not found: {', '.join(not_found)}\")\n\n    slices = [i for i in keys if isinstance(i, slice)]\n    slc = slice(0, len(self)) if not slices else slices[0]\n\n    if (\n        len(slices) == 0 and len(column_names) == 1\n    ):  # e.g. tbl['a'] or tbl['a'][:10]\n        col = self.columns[column_names[0]]\n        if slices:\n            return col[slc]  # return slice from column as list of values\n        else:\n            return col  # return whole column\n\n    elif len(integers) == 1:  # return a single tuple.\n        row_no = integers[0]\n        slc = slice(row_no, row_no + 1)\n        return tuple(self.columns[name][slc].tolist()[0] for name in column_names)\n\n    elif not slices:  # e.g. new table with N whole columns.\n        return self.__class__(\n            columns={name: self.columns[name] for name in column_names}\n        )\n\n    else:  # e.g. new table from selection of columns and slices.\n        t = self.__class__()\n        for name in column_names:\n            column = self.columns[name]\n\n            new_column = Column(t.path)  # create new Column.\n            for item in column.getpages(slc):\n                if isinstance(item, np.ndarray):\n                    new_column.extend(item)  # extend subslice (expensive)\n                elif isinstance(item, SimplePage):\n                    new_column.pages.append(item)  # extend page (cheap)\n                else:\n                    raise TypeError(f\"Bad item: {item}\")\n\n            # below:\n            # set the new column directly on t.columns.\n            # Do not use t[name] as that triggers __setitem__ again.\n            t.columns[name] = new_column\n\n        return t\n
"},{"location":"reference/base/#tablite.base.Table.__len__","title":"tablite.base.Table.__len__()","text":"Source code in tablite/base.py
def __len__(self):  # USER FUNCTION.\n    if not self.columns:\n        return 0\n    return max(len(c) for c in self.columns.values())\n
"},{"location":"reference/base/#tablite.base.Table.__eq__","title":"tablite.base.Table.__eq__(other) -> bool","text":"

Determines if two tables have identical content.

PARAMETER DESCRIPTION other

table for comparison

TYPE: Table

RETURNS DESCRIPTION bool

True if tables are identical.

TYPE: bool

Source code in tablite/base.py
def __eq__(self, other) -> bool:  # USER FUNCTION.\n    \"\"\"Determines if two tables have identical content.\n\n    Args:\n        other (Table): table for comparison\n\n    Returns:\n        bool: True if tables are identical.\n    \"\"\"\n    if isinstance(other, dict):\n        return self.items() == other.items()\n    if not isinstance(other, Table):\n        return False\n    if id(self) == id(other):\n        return True\n    if len(self) != len(other):\n        return False\n    if len(self) == len(other) == 0:\n        return True\n    if self.columns.keys() != other.columns.keys():\n        return False\n    for name, col in self.columns.items():\n        if not (col == other.columns[name]):\n            return False\n    return True\n
"},{"location":"reference/base/#tablite.base.Table.clear","title":"tablite.base.Table.clear()","text":"

clears the table. Like dict().clear()

Source code in tablite/base.py
def clear(self):  # USER FUNCTION.\n    \"\"\"clears the table. Like dict().clear()\"\"\"\n    self.columns.clear()\n
"},{"location":"reference/base/#tablite.base.Table.save","title":"tablite.base.Table.save(path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1)","text":"

saves table to compressed tpz file.

PARAMETER DESCRIPTION path

file destination.

TYPE: Path

compression_method

See zipfile compression methods. Defaults to ZIP_DEFLATED.

DEFAULT: ZIP_DEFLATED

compression_level

See zipfile compression levels. Defaults to 1.

DEFAULT: 1

The file format is as follows: .tpz is a gzip archive with table metadata captured as table.yml and the necessary set of pages saved as .npy files.

The zip contains table.yml which provides an overview of the data:

--------------------------------------\n%YAML 1.2                              yaml version\ncolumns:                               start of columns section.\n    name: \u201c\u5217 1\u201d                       name of column 1.\n        pages: [p1b1, p1b2]            list of pages in column 1.\n    name: \u201c\u5217 2\u201d                       name of column 2\n        pages: [p2b1, p2b2]            list of pages in column 2.\n----------------------------------------\n
Source code in tablite/base.py
def save(\n    self, path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1\n):  # USER FUNCTION.\n    \"\"\"saves table to compressed tpz file.\n\n    Args:\n        path (Path): file destination.\n        compression_method: See zipfile compression methods. Defaults to ZIP_DEFLATED.\n        compression_level: See zipfile compression levels. Defaults to 1.\n        The default settings produce 80% compression at 10% slowdown.\n\n    The file format is as follows:\n    .tpz is a gzip archive with table metadata captured as table.yml\n    and the necessary set of pages saved as .npy files.\n\n    The zip contains table.yml which provides an overview of the data:\n    ```\n    --------------------------------------\n    %YAML 1.2                              yaml version\n    columns:                               start of columns section.\n        name: \u201c\u5217 1\u201d                       name of column 1.\n            pages: [p1b1, p1b2]            list of pages in column 1.\n        name: \u201c\u5217 2\u201d                       name of column 2\n            pages: [p2b1, p2b2]            list of pages in column 2.\n    ----------------------------------------\n    ```\n    \"\"\"\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path += \".tpz\"\n\n    # create yaml document\n    _page_counter = 0\n    d = {}\n    cols = {}\n    for name, col in self.columns.items():\n        type_check(col, Column)\n        cols[name] = {\"pages\": [p.path.name for p in col.pages]}\n        _page_counter += len(col.pages)\n    d[\"columns\"] = cols\n    yml = yaml.safe_dump(\n        d, sort_keys=False, allow_unicode=True, default_flow_style=None\n    )\n\n    _file_counter = 0\n    with zipfile.ZipFile(\n        path, \"w\", compression=compression_method, compresslevel=compression_level\n    ) as f:\n        log.debug(f\"writing .tpz to {path} with\\n{yml}\")\n        f.writestr(\"table.yml\", yml)\n        for name, col in self.columns.items():\n            for page in set(\n                col.pages\n            ):  # set of pages! remember t *= 1000 repeats t 1000x\n                with open(page.path, \"rb\", buffering=0) as raw_io:\n                    f.writestr(page.path.name, raw_io.read())\n                _file_counter += 1\n                log.debug(f\"adding Page {page.path}\")\n\n        _fields = len(self) * len(self.columns)\n        _avg = _fields // _page_counter\n        log.debug(\n            f\"Wrote {_fields:,} on {_page_counter:,} pages in {_file_counter} files: {_avg} fields/page\"\n        )\n
"},{"location":"reference/base/#tablite.base.Table.load","title":"tablite.base.Table.load(path, tqdm=_tqdm) classmethod","text":"

loads a table from .tpz file. See also Table.save for details on the file format.

PARAMETER DESCRIPTION path

source file

TYPE: Path

RETURNS DESCRIPTION Table

table in read-only mode.

Source code in tablite/base.py
@classmethod\ndef load(cls, path, tqdm=_tqdm):  # USER FUNCTION.\n    \"\"\"loads a table from .tpz file.\n    See also Table.save for details on the file format.\n\n    Args:\n        path (Path): source file\n\n    Returns:\n        Table: table in read-only mode.\n    \"\"\"\n    type_check(path, Path)\n    log.debug(f\"loading {path}\")\n    with zipfile.ZipFile(path, \"r\") as f:\n        yml = f.read(\"table.yml\")\n        metadata = yaml.safe_load(yml)\n        t = cls()\n\n        page_count = sum([len(c[\"pages\"]) for c in metadata[\"columns\"].values()])\n\n        with tqdm(\n            total=page_count,\n            desc=f\"loading '{path.name}' file\",\n            disable=Config.TQDM_DISABLE,\n        ) as pbar:\n            for name, d in metadata[\"columns\"].items():\n                column = Column(t.path)\n                for page in d[\"pages\"]:\n                    bytestream = io.BytesIO(f.read(page))\n                    data = np.load(bytestream, allow_pickle=True, fix_imports=False)\n                    column.extend(data)\n                    pbar.update(1)\n                t.columns[name] = column\n    update_access_time(path)\n    return t\n
"},{"location":"reference/base/#tablite.base.Table.copy","title":"tablite.base.Table.copy()","text":"Source code in tablite/base.py
def copy(self):\n    cls = type(self)\n    t = cls()\n    for name, column in self.columns.items():\n        new = Column(t.path)\n        new.pages = column.pages[:]\n        t.columns[name] = new\n    return t\n
"},{"location":"reference/base/#tablite.base.Table.__imul__","title":"tablite.base.Table.__imul__(other)","text":"

Repeats instance of table N times.

Like list: t = t * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"Repeats instance of table N times.\n\n    Like list: `t = t * N`\n\n    Args:\n        other (int): multiplier\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a table can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    for col in self.columns.values():\n        col *= other\n    return self\n
"},{"location":"reference/base/#tablite.base.Table.__mul__","title":"tablite.base.Table.__mul__(other)","text":"

Repeat table N times. Like list: new = old * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"Repeat table N times.\n    Like list: `new = old * N`\n\n    Args:\n        other (int): multiplier\n\n    Returns:\n        Table\n    \"\"\"\n    new = self.copy()\n    return new.__imul__(other)\n
"},{"location":"reference/base/#tablite.base.Table.__iadd__","title":"tablite.base.Table.__iadd__(other)","text":"

Concatenates tables with same column names.

Like list: table_1 += table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION None

self is updated.

Source code in tablite/base.py
def __iadd__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_1 += table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        None: self is updated.\n    \"\"\"\n    type_check(other, Table)\n    for name in self.columns.keys():\n        if name not in other.columns:\n            raise ValueError(f\"{name} not in other\")\n    for name in other.columns.keys():\n        if name not in self.columns:\n            raise ValueError(f\"{name} missing from self\")\n\n    for name, column in self.columns.items():\n        other_col = other.columns.get(name, None)\n        column.pages.extend(other_col.pages[:])\n    return self\n
"},{"location":"reference/base/#tablite.base.Table.__add__","title":"tablite.base.Table.__add__(other)","text":"

Concatenates tables with same column names.

Like list: table_3 = table_1 + table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __add__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_3 = table_1 + table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        Table\n    \"\"\"\n    type_check(other, Table)\n    cp = self.copy()\n    cp += other\n    return cp\n
"},{"location":"reference/base/#tablite.base.Table.add_rows","title":"tablite.base.Table.add_rows(*args, **kwargs)","text":"

its more efficient to add many rows at once.

if both args and kwargs, then args are added first, followed by kwargs.

supported cases:

>>> t = Table()\n>>> t.add_columns('row','A','B','C')\n>>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n>>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n>>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n>>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n>>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n>>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n>>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n>>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n>>> t.add_rows(\n    {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n    )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n>>> t.add_rows( *[\n    {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n    ])                                                  # (10) list of dicts as args\n>>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n
Source code in tablite/base.py
def add_rows(self, *args, **kwargs):\n    \"\"\"its more efficient to add many rows at once.\n\n    if both args and kwargs, then args are added first, followed by kwargs.\n\n    supported cases:\n    ```\n    >>> t = Table()\n    >>> t.add_columns('row','A','B','C')\n    >>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n    >>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n    >>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n    >>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n    >>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n    >>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n    >>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n    >>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n    >>> t.add_rows(\n        {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n        )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n    >>> t.add_rows( *[\n        {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n        ])                                                  # (10) list of dicts as args\n    >>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n    ```\n\n    \"\"\"\n    if not Table._add_row_slow_warning:\n        warnings.warn(\n            \"add_rows is slow. Consider using add_columns and then assigning values to the columns directly.\"\n        )\n        Table._add_row_slow_warning = True\n\n    if args:\n        if not all(isinstance(i, (list, tuple, dict)) for i in args):  # 1,4\n            args = [args]\n\n        if all(isinstance(i, (list, tuple, dict)) for i in args):  # 2,3,7,8\n            # 1. turn the data into columns:\n\n            d = {n: [] for n in self.columns}\n            for arg in args:\n                if len(arg) != len(self.columns):\n                    raise ValueError(\n                        f\"len({arg})== {len(arg)}, but there are {len(self.columns)} columns\"\n                    )\n\n                if isinstance(arg, dict):\n                    for k, v in arg.items():  # 7,8\n                        d[k].append(v)\n\n                elif isinstance(arg, (list, tuple)):  # 2,3\n                    for n, v in zip(self.columns, arg):\n                        d[n].append(v)\n\n                else:\n                    raise TypeError(f\"{arg}?\")\n            # 2. extend the columns\n            for n, values in d.items():\n                col = self.columns[n]\n                col.extend(list_to_np_array(values))\n\n    if kwargs:\n        if isinstance(kwargs, dict):\n            if all(isinstance(v, (list, tuple)) for v in kwargs.values()):\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(list_to_np_array(v))\n            else:\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(np.array([v]))\n        else:\n            raise ValueError(f\"format not recognised: {kwargs}\")\n\n    return\n
"},{"location":"reference/base/#tablite.base.Table.add_columns","title":"tablite.base.Table.add_columns(*names)","text":"

Adds column names to table.

Source code in tablite/base.py
def add_columns(self, *names):\n    \"\"\"Adds column names to table.\"\"\"\n    for name in names:\n        self.columns[name] = Column(self.path)\n
"},{"location":"reference/base/#tablite.base.Table.add_column","title":"tablite.base.Table.add_column(name, data=None)","text":"

verbose alias for table[name] = data, that checks if name already exists

PARAMETER DESCRIPTION name

column name

TYPE: str

data

values. Defaults to None.

TYPE: list,tuple) DEFAULT: None

RAISES DESCRIPTION TypeError

name isn't string

ValueError

name already exists

Source code in tablite/base.py
def add_column(self, name, data=None):\n    \"\"\"verbose alias for table[name] = data, that checks if name already exists\n\n    Args:\n        name (str): column name\n        data ((list,tuple), optional): values. Defaults to None.\n\n    Raises:\n        TypeError: name isn't string\n        ValueError: name already exists\n    \"\"\"\n    if not isinstance(name, str):\n        raise TypeError(\"expected name as string\")\n    if name in self.columns:\n        raise ValueError(f\"{name} already in {self.columns}\")\n    self.__setitem__(name, data)\n
"},{"location":"reference/base/#tablite.base.Table.stack","title":"tablite.base.Table.stack(other)","text":"

returns the joint stack of tables with overlapping column names. Example:

| Table A|  +  | Table B| = |  Table AB |\n| A| B| C|     | A| B| D|   | A| B| C| -|\n                            | A| B| -| D|\n
Source code in tablite/base.py
def stack(self, other):\n    \"\"\"\n    returns the joint stack of tables with overlapping column names.\n    Example:\n    ```\n    | Table A|  +  | Table B| = |  Table AB |\n    | A| B| C|     | A| B| D|   | A| B| C| -|\n                                | A| B| -| D|\n    ```\n    \"\"\"\n    if not isinstance(other, Table):\n        raise TypeError(f\"stack only works for Table, not {type(other)}\")\n\n    cp = self.copy()\n    for name, col2 in other.columns.items():\n        if name not in cp.columns:\n            cp[name] = [None] * len(self)\n        cp[name].pages.extend(col2.pages[:])\n\n    for name in self.columns:\n        if name not in other.columns:\n            if len(cp) > 0:\n                cp[name].extend(np.array([None] * len(other)))\n    return cp\n
"},{"location":"reference/base/#tablite.base.Table.types","title":"tablite.base.Table.types()","text":"

returns nested dict of data types in the form: {column name: {python type class: number of instances }, ... }

example:

>>> t.types()\n{\n    'A': {<class 'str'>: 7},\n    'B': {<class 'int'>: 7}\n}\n
Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns nested dict of data types in the form:\n    `{column name: {python type class: number of instances }, ... }`\n\n    example:\n    ```\n    >>> t.types()\n    {\n        'A': {<class 'str'>: 7},\n        'B': {<class 'int'>: 7}\n    }\n    ```\n    \"\"\"\n    d = {}\n    for name, col in self.columns.items():\n        assert isinstance(col, Column)\n        d[name] = col.types()\n    return d\n
"},{"location":"reference/base/#tablite.base.Table.display_dict","title":"tablite.base.Table.display_dict(slice_=None, blanks=None, dtype=False)","text":"

helper for creating dict for display.

PARAMETER DESCRIPTION slice_

python slice. Defaults to None.

TYPE: slice DEFAULT: None

blanks

fill value for None. Defaults to None.

TYPE: optional DEFAULT: None

dtype

Adds datatype to each column. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION TypeError

slice_ must be None or slice.

RETURNS DESCRIPTION dict

from Table.

Source code in tablite/base.py
def display_dict(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"helper for creating dict for display.\n\n    Args:\n        slice_ (slice, optional): python slice. Defaults to None.\n        blanks (optional): fill value for `None`. Defaults to None.\n        dtype (bool, optional): Adds datatype to each column. Defaults to False.\n\n    Raises:\n        TypeError: slice_ must be None or slice.\n\n    Returns:\n        dict: from Table.\n    \"\"\"\n    if not self.columns:\n        print(\"Empty Table\")\n        return\n\n    def datatype(col):  # PRIVATE\n        \"\"\"creates label for column datatype.\"\"\"\n        types = col.types()\n        if len(types) == 1:\n            dt, _ = types.popitem()\n            typ = dt.__name__\n        else:\n            typ = \"mixed\"\n        return typ\n\n    row_count_tags = [\"#\", \"~\", \"*\"]\n    cols = set(self.columns)\n    for n, tag in product(range(1, 6), row_count_tags):\n        if n * tag not in cols:\n            tag = n * tag\n            break\n\n    if not isinstance(slice_, (slice, type(None))):\n        raise TypeError(f\"slice_ must be None or slice, not {type(slice_)}\")\n    if isinstance(slice_, slice):\n        slc = slice_\n    if slice_ is None:\n        if len(self) <= 20:\n            slc = slice(0, 20, 1)\n        else:\n            slc = None\n\n    n = len(self)\n    if slc:  # either we want slc or we want everything.\n        row_no = list(range(*slc.indices(len(self))))\n        data = {tag: [f\"{i:,}\".rjust(2) for i in row_no]}\n        for name, col in self.columns.items():\n            data[name] = list(chain(iter(col), repeat(blanks, times=n - len(col))))[\n                slc\n            ]\n    else:\n        data = {}\n        j = int(math.ceil(math.log10(n)) / 3) + len(str(n))\n        row_no = (\n            [f\"{i:,}\".rjust(j) for i in range(7)]\n            + [\"...\"]\n            + [f\"{i:,}\".rjust(j) for i in range(n - 7, n)]\n        )\n        data = {tag: row_no}\n\n        for name, col in self.columns.items():\n            if len(col) == n:\n                row = col[:7].tolist() + [\"...\"] + col[-7:].tolist()\n            else:\n                empty = [blanks] * 7\n                head = (col[:7].tolist() + empty)[:7]\n                tail = (col[n - 7 :].tolist() + empty)[-7:]\n                row = head + [\"...\"] + tail\n            data[name] = row\n\n    if dtype:\n        for name, values in data.items():\n            if name in self.columns:\n                col = self.columns[name]\n                values.insert(0, datatype(col))\n            else:\n                values.insert(0, \"row\")\n\n    return data\n
"},{"location":"reference/base/#tablite.base.Table.to_ascii","title":"tablite.base.Table.to_ascii(slice_=None, blanks=None, dtype=False)","text":"

returns ascii view of table as string.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def to_ascii(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"returns ascii view of table as string.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n\n    def adjust(v, length):  # PRIVATE FUNCTION\n        \"\"\"whitespace justifies field values based on datatype\"\"\"\n        if v is None:\n            return str(blanks).ljust(length)\n        elif isinstance(v, str):\n            return v.ljust(length)\n        else:\n            return str(v).rjust(length)\n\n    if not self.columns:\n        return str(self)\n\n    d = {}\n    for name, values in self.display_dict(\n        slice_=slice_, blanks=blanks, dtype=dtype\n    ).items():\n        as_text = [str(v) for v in values] + [str(name)]\n        width = max(len(i) for i in as_text)\n        new_name = name.center(width, \" \")\n        if dtype:\n            values[0] = values[0].center(width, \" \")\n        d[new_name] = [adjust(v, width) for v in values]\n\n    rows = dict_to_rows(d)\n    s = []\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n    s.append(\"|\" + \"|\".join(rows[0]) + \"|\")  # column names\n    start = 1\n    if dtype:\n        s.append(\"|\" + \"|\".join(rows[1]) + \"|\")  # datatypes\n        start = 2\n\n    s.append(\"+\" + \"+\".join([\"-\" * len(n) for n in rows[0]]) + \"+\")\n    for row in rows[start:]:\n        s.append(\"|\" + \"|\".join(row) + \"|\")\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n\n    if len(set(len(c) for c in self.columns.values())) != 1:\n        warning = f\"Warning: Columns have different lengths. {blanks} is used as fill value.\"\n        s.append(warning)\n\n    return \"\\n\".join(s)\n
"},{"location":"reference/base/#tablite.base.Table.show","title":"tablite.base.Table.show(slice_=None, blanks=None, dtype=False)","text":"

prints ascii view of table.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def show(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"prints ascii view of table.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n    print(self.to_ascii(slice_=slice_, blanks=blanks, dtype=dtype))\n
"},{"location":"reference/base/#tablite.base.Table.to_dict","title":"tablite.base.Table.to_dict(columns=None, slice_=None)","text":"

columns: list of column names. Default is None == all columns. slice_: slice. Default is None == all rows.

returns: dict with columns as keys and lists of values.

Example:

>>> t.show()\n+===+===+===+\n| # | a | b |\n|row|int|int|\n+---+---+---+\n| 0 |  1|  3|\n| 1 |  2|  4|\n+===+===+===+\n>>> t.to_dict()\n{'a':[1,2], 'b':[3,4]}\n
Source code in tablite/base.py
def to_dict(self, columns=None, slice_=None):\n    \"\"\"\n    columns: list of column names. Default is None == all columns.\n    slice_: slice. Default is None == all rows.\n\n    returns: dict with columns as keys and lists of values.\n\n    Example:\n    ```\n    >>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  3|\n    | 1 |  2|  4|\n    +===+===+===+\n    >>> t.to_dict()\n    {'a':[1,2], 'b':[3,4]}\n    ```\n\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n    assert isinstance(slice_, slice)\n\n    if columns is None:\n        columns = list(self.columns.keys())\n    if not isinstance(columns, list):\n        raise TypeError(\"expected columns as list of strings\")\n\n    return {name: list(self.columns[name][slice_]) for name in columns}\n
"},{"location":"reference/base/#tablite.base.Table.as_json_serializable","title":"tablite.base.Table.as_json_serializable(row_count='row id', start_on=1, columns=None, slice_=None)","text":"

provides a JSON compatible format of the table.

PARAMETER DESCRIPTION row_count

Label for row counts. Defaults to \"row id\".

TYPE: str DEFAULT: 'row id'

start_on

row counts starts by default on 1.

TYPE: int DEFAULT: 1

columns

Column names. Defaults to None which returns all columns.

TYPE: list of str DEFAULT: None

slice_

selector. Defaults to None which returns [:]

TYPE: slice DEFAULT: None

RETURNS DESCRIPTION

JSON serializable dict: All python datatypes have been converted to JSON compliant data.

Source code in tablite/base.py
def as_json_serializable(\n    self, row_count=\"row id\", start_on=1, columns=None, slice_=None\n):\n    \"\"\"provides a JSON compatible format of the table.\n\n    Args:\n        row_count (str, optional): Label for row counts. Defaults to \"row id\".\n        start_on (int, optional): row counts starts by default on 1.\n        columns (list of str, optional): Column names.\n            Defaults to None which returns all columns.\n        slice_ (slice, optional): selector. Defaults to None which returns [:]\n\n    Returns:\n        JSON serializable dict: All python datatypes have been converted to JSON compliant data.\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n\n    assert isinstance(slice_, slice)\n    new = {\"columns\": {}, \"total_rows\": len(self)}\n    if row_count is not None:\n        new[\"columns\"][row_count] = [\n            i + start_on for i in range(*slice_.indices(len(self)))\n        ]\n\n    d = self.to_dict(columns, slice_=slice_)\n    for k, data in d.items():\n        new_k = unique_name(\n            k, new[\"columns\"]\n        )  # used to avoid overwriting the `row id` key.\n        new[\"columns\"][new_k] = [\n            DataTypes.to_json(v) for v in data\n        ]  # deal with non-json datatypes.\n    return new\n
"},{"location":"reference/base/#tablite.base.Table.index","title":"tablite.base.Table.index(*args)","text":"

param: *args: column names returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}

Examples:

>>> table6 = Table()\n>>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n>>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n
>>> table6.index('A')  # single key.\n{('Alice',): [0],\n ('Bob',): [1, 2],\n ('Ben',): [3, 5],\n ('Charlie',): [4],\n ('Albert',): [6]})\n
>>> table6.index('A', 'B')  # multiple keys.\n{('Alice', 'Alison'): [0],\n ('Bob', 'Marley'): [1],\n ('Bob', 'Dylan'): [2],\n ('Ben', 'Affleck'): [3],\n ('Charlie', 'Hepburn'): [4],\n ('Ben', 'Barnes'): [5],\n ('Albert', 'Einstein'): [6]})\n
Source code in tablite/base.py
def index(self, *args):\n    \"\"\"\n    param: *args: column names\n    returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}\n\n    Examples:\n        ```\n        >>> table6 = Table()\n        >>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n        >>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n        ```\n\n        ```\n        >>> table6.index('A')  # single key.\n        {('Alice',): [0],\n         ('Bob',): [1, 2],\n         ('Ben',): [3, 5],\n         ('Charlie',): [4],\n         ('Albert',): [6]})\n        ```\n\n        ```\n        >>> table6.index('A', 'B')  # multiple keys.\n        {('Alice', 'Alison'): [0],\n         ('Bob', 'Marley'): [1],\n         ('Bob', 'Dylan'): [2],\n         ('Ben', 'Affleck'): [3],\n         ('Charlie', 'Hepburn'): [4],\n         ('Ben', 'Barnes'): [5],\n         ('Albert', 'Einstein'): [6]})\n        ```\n\n    \"\"\"\n    idx = defaultdict(list)\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in enumerate(zip(*iterators)):\n        key = tuple(numpy_to_python(k) for k in key)\n        idx[key].append(ix)\n    return idx\n
"},{"location":"reference/base/#tablite.base.Table.unique_index","title":"tablite.base.Table.unique_index(*args, tqdm=_tqdm)","text":"

generates the index of unique rows given a list of column names

PARAMETER DESCRIPTION *args

columns names

TYPE: any DEFAULT: ()

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION

np.array(int64): indices of unique records.

Source code in tablite/base.py
def unique_index(self, *args, tqdm=_tqdm):\n    \"\"\"generates the index of unique rows given a list of column names\n\n    Args:\n        *args (any): columns names\n        tqdm (tqdm, optional): Defaults to _tqdm.\n\n    Returns:\n        np.array(int64): indices of unique records.\n    \"\"\"\n    if not args:\n        raise ValueError(\"*args (column names) is required\")\n    seen = set()\n    unique = set()\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in tqdm(enumerate(zip(*iterators)), disable=Config.TQDM_DISABLE):\n        key_hash = hash(tuple(numpy_to_python(k) for k in key))\n        if key_hash in seen:\n            continue\n        else:\n            seen.add(key_hash)\n            unique.add(ix)\n    return np.array(sorted(unique))\n
"},{"location":"reference/base/#tablite.base-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.register","title":"tablite.base.register(path)","text":"

registers path in file_registry

The method is used by Table during init when the working directory path is set, so that python can clean all temporary files up at exit.

PARAMETER DESCRIPTION path

typically tmp/tablite-tmp/PID-{os.getpid()}

TYPE: Path

Source code in tablite/base.py
def register(path):\n    \"\"\"registers path in file_registry\n\n    The method is used by Table during init when the working directory path\n    is set, so that python can clean all temporary files up at exit.\n\n    Args:\n        path (Path): typically tmp/tablite-tmp/PID-{os.getpid()}\n    \"\"\"\n    global file_registry\n    file_registry.add(path)\n
"},{"location":"reference/base/#tablite.base.shutdown","title":"tablite.base.shutdown()","text":"

method to clean up temporary files triggered at shutdown.

Source code in tablite/base.py
def shutdown():\n    \"\"\"method to clean up temporary files triggered at shutdown.\"\"\"\n    for path in file_registry:\n        if Config.pid in str(path):  # safety feature to prevent rm -rf /\n            log.debug(f\"shutdown: running rmtree({path})\")\n            shutil.rmtree(path)\n
"},{"location":"reference/config/","title":"Config","text":""},{"location":"reference/config/#tablite.config","title":"tablite.config","text":""},{"location":"reference/config/#tablite.config-classes","title":"Classes","text":""},{"location":"reference/config/#tablite.config.Config","title":"tablite.config.Config","text":"

Bases: object

Config class for Tablite Tables.

The default location for the storage is loaded as

Config.workdir = pathlib.Path(os.environ.get(\"TABLITE_TMPDIR\", f\"{tempfile.gettempdir()}/tablite-tmp\"))

to overwrite, first import the config class, then set the new workdir.

>>> from tablite import config\n>>> from pathlib import Path\n>>> config.workdir = Path(\"/this/new/location\")\n

for every new table or record this path will be used.

PAGE_SIZE = 1_000_000 sets the page size limit.

Multiprocessing is enabled in one of three modes: AUTO = \"auto\" FALSE = \"sp\" FORCE = \"mp\"

MULTIPROCESSING_MODE = AUTO is default.

SINGLE_PROCESSING_LIMIT = 1_000_000 when the number of fields (rows x columns) exceed this value, multiprocessing is used.

"},{"location":"reference/config/#tablite.config.Config-attributes","title":"Attributes","text":""},{"location":"reference/config/#tablite.config.Config.USE_NIMPORTER","title":"tablite.config.Config.USE_NIMPORTER = os.environ.get('USE_NIMPORTER', 'true').lower() in ['1', 't', 'true', 'y', 'yes'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.ALLOW_CSV_READER_FALLTHROUGH","title":"tablite.config.Config.ALLOW_CSV_READER_FALLTHROUGH = os.environ.get('ALLOW_CSV_READER_FALLTHROUGH', 'true').lower() in ['1', 't', 'true', 'y', 'yes'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.NIM_SUPPORTED_CONV_TYPES","title":"tablite.config.Config.NIM_SUPPORTED_CONV_TYPES = ['Windows-1252', 'ISO-8859-1'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.workdir","title":"tablite.config.Config.workdir = pathlib.Path(os.environ.get('TABLITE_TMPDIR', f'{tempfile.gettempdir()}/tablite-tmp')) class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.pid","title":"tablite.config.Config.pid = f'pid-{os.getpid()}' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.PAGE_SIZE","title":"tablite.config.Config.PAGE_SIZE = 1000000 class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.ENCODING","title":"tablite.config.Config.ENCODING = 'UTF-8' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.DISK_LIMIT","title":"tablite.config.Config.DISK_LIMIT = int(10000000000.0) class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.SINGLE_PROCESSING_LIMIT","title":"tablite.config.Config.SINGLE_PROCESSING_LIMIT = 1000000 class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.AUTO","title":"tablite.config.Config.AUTO = 'auto' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.FALSE","title":"tablite.config.Config.FALSE = 'sp' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.FORCE","title":"tablite.config.Config.FORCE = 'mp' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.MULTIPROCESSING_MODE","title":"tablite.config.Config.MULTIPROCESSING_MODE = AUTO class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.TQDM_DISABLE","title":"tablite.config.Config.TQDM_DISABLE = False class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config-functions","title":"Functions","text":""},{"location":"reference/config/#tablite.config.Config.reset","title":"tablite.config.Config.reset() classmethod","text":"

Resets the config class to original values.

Source code in tablite/config.py
@classmethod\ndef reset(cls):\n    \"\"\"Resets the config class to original values.\"\"\"\n    for k, v in _default_values.items():\n        setattr(Config, k, v)\n
"},{"location":"reference/config/#tablite.config.Config.page_steps","title":"tablite.config.Config.page_steps(length) classmethod","text":"

an iterator that yield start and end in page sizes

YIELDS DESCRIPTION tuple

start:int, end:int

Source code in tablite/config.py
@classmethod\ndef page_steps(cls, length):\n    \"\"\"an iterator that yield start and end in page sizes\n\n    Yields:\n        tuple: start:int, end:int\n    \"\"\"\n    start, end = 0, 0\n    for _ in range(0, length + 1, cls.PAGE_SIZE):\n        start, end = end, min(end + cls.PAGE_SIZE, length)\n        yield start, end\n        if end == length:\n            return\n
"},{"location":"reference/core/","title":"Core","text":""},{"location":"reference/core/#tablite.core","title":"tablite.core","text":""},{"location":"reference/core/#tablite.core-attributes","title":"Attributes","text":""},{"location":"reference/core/#tablite.core.log","title":"tablite.core.log = logging.getLogger(__name__) module-attribute","text":""},{"location":"reference/core/#tablite.core-classes","title":"Classes","text":""},{"location":"reference/core/#tablite.core.Table","title":"tablite.core.Table(columns=None, headers=None, rows=None, _path=None)","text":"

Bases: Table

creates Table

PARAMETER DESCRIPTION EITHER

columns (dict, optional): dict with column names as keys, values as lists. Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})

Source code in tablite/core.py
def __init__(self, columns=None, headers=None, rows=None, _path=None) -> None:\n    \"\"\"creates Table\n\n    Args:\n        EITHER:\n            columns (dict, optional): dict with column names as keys, values as lists.\n            Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})\n        OR\n            headers (list of strings, optional): list of column names.\n            rows (list of tuples or lists, optional): values for columns\n            Example: t = Table(headers=[\"a\", \"b\"], rows=[[1,3], [2,4]])\n    \"\"\"\n    super().__init__(columns, headers, rows, _path)\n
"},{"location":"reference/core/#tablite.core.Table-attributes","title":"Attributes","text":""},{"location":"reference/core/#tablite.core.Table.path","title":"tablite.core.Table.path = _path instance-attribute","text":""},{"location":"reference/core/#tablite.core.Table.columns","title":"tablite.core.Table.columns = {} instance-attribute","text":""},{"location":"reference/core/#tablite.core.Table.rows","title":"tablite.core.Table.rows property","text":"

enables row based iteration in python types.

Example:

for row in Table.rows:\n    print(row)\n

Yields: tuple: values is same order as columns.

"},{"location":"reference/core/#tablite.core.Table-functions","title":"Functions","text":""},{"location":"reference/core/#tablite.core.Table.__str__","title":"tablite.core.Table.__str__()","text":"Source code in tablite/base.py
def __str__(self):  # USER FUNCTION.\n    return f\"{self.__class__.__name__}({len(self.columns):,} columns, {len(self):,} rows)\"\n
"},{"location":"reference/core/#tablite.core.Table.__repr__","title":"tablite.core.Table.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return self.__str__()\n
"},{"location":"reference/core/#tablite.core.Table.nbytes","title":"tablite.core.Table.nbytes()","text":"

finds the total bytes of the table on disk

RETURNS DESCRIPTION tuple

int: real bytes used on disk int: total bytes used if flattened

Source code in tablite/base.py
def nbytes(self):  # USER FUNCTION.\n    \"\"\"finds the total bytes of the table on disk\n\n    Returns:\n        tuple:\n            int: real bytes used on disk\n            int: total bytes used if flattened\n    \"\"\"\n    real = {}\n    total = 0\n    for column in self.columns.values():\n        for page in set(column.pages):\n            real[page] = page.path.stat().st_size\n        for page in column.pages:\n            total += real[page]\n    return sum(real.values()), total\n
"},{"location":"reference/core/#tablite.core.Table.items","title":"tablite.core.Table.items()","text":"

returns table as dict

RETURNS DESCRIPTION dict

Table as dict {column_name: [values], ...}

Source code in tablite/base.py
def items(self):  # USER FUNCTION.\n    \"\"\"returns table as dict\n\n    Returns:\n        dict: Table as dict `{column_name: [values], ...}`\n    \"\"\"\n    return {\n        name: column[:].tolist() for name, column in self.columns.items()\n    }.items()\n
"},{"location":"reference/core/#tablite.core.Table.__delitem__","title":"tablite.core.Table.__delitem__(key)","text":"

Examples:

>>> del table['a']  # removes column 'a'\n>>> del table[-3:]  # removes last 3 rows from all columns.\n
Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION.\n    \"\"\"\n    Examples:\n    ```\n    >>> del table['a']  # removes column 'a'\n    >>> del table[-3:]  # removes last 3 rows from all columns.\n    ```\n    \"\"\"\n    if isinstance(key, (int, slice)):\n        for column in self.columns.values():\n            del column[key]\n    elif key in self.columns:\n        del self.columns[key]\n    else:\n        raise KeyError(f\"Key not found: {key}\")\n
"},{"location":"reference/core/#tablite.core.Table.__setitem__","title":"tablite.core.Table.__setitem__(key, value)","text":"

table behaves like a dict. Args: key (str or hashable): column name value (iterable): list, tuple or nd.array with values.

As Table now accepts the keyword columns as a dict:

>>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n

and the header/data combinations:

>>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n

This has the side-benefit that tuples now can be used as headers.

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION\n    \"\"\"table behaves like a dict.\n    Args:\n        key (str or hashable): column name\n        value (iterable): list, tuple or nd.array with values.\n\n    As Table now accepts the keyword `columns` as a dict:\n    ```\n    >>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n    ```\n    and the header/data combinations:\n    ```\n    >>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n    ```\n    This has the side-benefit that tuples now can be used as headers.\n    \"\"\"\n    if value is None:\n        self.columns[key] = Column(self.path, value=None)\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, (np.ndarray)):\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, Column):\n        self.columns[key] = value\n    else:\n        raise TypeError(f\"{type(value)} not supported.\")\n
"},{"location":"reference/core/#tablite.core.Table.__getitem__","title":"tablite.core.Table.__getitem__(keys)","text":"

Enables selection of columns and rows

PARAMETER DESCRIPTION keys

TYPE: column name, integer or slice

Examples

>>>

10] selects first 10 rows from all columns

TYPE: table[

>>>

20:3] selects column 'b' and 'c' and 'a' twice for a slice.

TYPE: table['b', 'a', 'a', 'c', 2

Raises: KeyError: if key is not found. TypeError: if key is not a string, integer or slice.

RETURNS DESCRIPTION Table

returns columns in same order as selection.

Source code in tablite/base.py
def __getitem__(self, keys):  # USER FUNCTION\n    \"\"\"\n    Enables selection of columns and rows\n\n    Args:\n        keys (column name, integer or slice):\n        Examples:\n        ```\n        >>> table['a']                        selects column 'a'\n        >>> table[3]                          selects row 3 as a tuple.\n        >>> table[:10]                        selects first 10 rows from all columns\n        >>> table['a','b', slice(3,20,2)]     selects a slice from columns 'a' and 'b'\n        >>> table['b', 'a', 'a', 'c', 2:20:3] selects column 'b' and 'c' and 'a' twice for a slice.\n        >>> table[('b', 'a', 'a', 'c')]       selects columns 'b', 'a', 'a', and 'c' using a tuple.\n        ```\n    Raises:\n        KeyError: if key is not found.\n        TypeError: if key is not a string, integer or slice.\n\n    Returns:\n        Table: returns columns in same order as selection.\n    \"\"\"\n\n    if not isinstance(keys, tuple):\n        if isinstance(keys, list):\n            keys = tuple(keys)\n        else:\n            keys = (keys,)\n    if isinstance(keys[0], tuple):\n        keys = tuple(list(chain(*keys)))\n\n    integers = [i for i in keys if isinstance(i, int)]\n    if len(integers) == len(keys) == 1:  # return a single tuple.\n        keys = [slice(keys[0])]\n\n    column_names = [i for i in keys if isinstance(i, str)]\n    column_names = list(self.columns) if not column_names else column_names\n    not_found = [name for name in column_names if name not in self.columns]\n    if not_found:\n        raise KeyError(f\"keys not found: {', '.join(not_found)}\")\n\n    slices = [i for i in keys if isinstance(i, slice)]\n    slc = slice(0, len(self)) if not slices else slices[0]\n\n    if (\n        len(slices) == 0 and len(column_names) == 1\n    ):  # e.g. tbl['a'] or tbl['a'][:10]\n        col = self.columns[column_names[0]]\n        if slices:\n            return col[slc]  # return slice from column as list of values\n        else:\n            return col  # return whole column\n\n    elif len(integers) == 1:  # return a single tuple.\n        row_no = integers[0]\n        slc = slice(row_no, row_no + 1)\n        return tuple(self.columns[name][slc].tolist()[0] for name in column_names)\n\n    elif not slices:  # e.g. new table with N whole columns.\n        return self.__class__(\n            columns={name: self.columns[name] for name in column_names}\n        )\n\n    else:  # e.g. new table from selection of columns and slices.\n        t = self.__class__()\n        for name in column_names:\n            column = self.columns[name]\n\n            new_column = Column(t.path)  # create new Column.\n            for item in column.getpages(slc):\n                if isinstance(item, np.ndarray):\n                    new_column.extend(item)  # extend subslice (expensive)\n                elif isinstance(item, SimplePage):\n                    new_column.pages.append(item)  # extend page (cheap)\n                else:\n                    raise TypeError(f\"Bad item: {item}\")\n\n            # below:\n            # set the new column directly on t.columns.\n            # Do not use t[name] as that triggers __setitem__ again.\n            t.columns[name] = new_column\n\n        return t\n
"},{"location":"reference/core/#tablite.core.Table.__len__","title":"tablite.core.Table.__len__()","text":"Source code in tablite/base.py
def __len__(self):  # USER FUNCTION.\n    if not self.columns:\n        return 0\n    return max(len(c) for c in self.columns.values())\n
"},{"location":"reference/core/#tablite.core.Table.__eq__","title":"tablite.core.Table.__eq__(other) -> bool","text":"

Determines if two tables have identical content.

PARAMETER DESCRIPTION other

table for comparison

TYPE: Table

RETURNS DESCRIPTION bool

True if tables are identical.

TYPE: bool

Source code in tablite/base.py
def __eq__(self, other) -> bool:  # USER FUNCTION.\n    \"\"\"Determines if two tables have identical content.\n\n    Args:\n        other (Table): table for comparison\n\n    Returns:\n        bool: True if tables are identical.\n    \"\"\"\n    if isinstance(other, dict):\n        return self.items() == other.items()\n    if not isinstance(other, Table):\n        return False\n    if id(self) == id(other):\n        return True\n    if len(self) != len(other):\n        return False\n    if len(self) == len(other) == 0:\n        return True\n    if self.columns.keys() != other.columns.keys():\n        return False\n    for name, col in self.columns.items():\n        if not (col == other.columns[name]):\n            return False\n    return True\n
"},{"location":"reference/core/#tablite.core.Table.clear","title":"tablite.core.Table.clear()","text":"

clears the table. Like dict().clear()

Source code in tablite/base.py
def clear(self):  # USER FUNCTION.\n    \"\"\"clears the table. Like dict().clear()\"\"\"\n    self.columns.clear()\n
"},{"location":"reference/core/#tablite.core.Table.save","title":"tablite.core.Table.save(path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1)","text":"

saves table to compressed tpz file.

PARAMETER DESCRIPTION path

file destination.

TYPE: Path

compression_method

See zipfile compression methods. Defaults to ZIP_DEFLATED.

DEFAULT: ZIP_DEFLATED

compression_level

See zipfile compression levels. Defaults to 1.

DEFAULT: 1

The file format is as follows: .tpz is a gzip archive with table metadata captured as table.yml and the necessary set of pages saved as .npy files.

The zip contains table.yml which provides an overview of the data:

--------------------------------------\n%YAML 1.2                              yaml version\ncolumns:                               start of columns section.\n    name: \u201c\u5217 1\u201d                       name of column 1.\n        pages: [p1b1, p1b2]            list of pages in column 1.\n    name: \u201c\u5217 2\u201d                       name of column 2\n        pages: [p2b1, p2b2]            list of pages in column 2.\n----------------------------------------\n
Source code in tablite/base.py
def save(\n    self, path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1\n):  # USER FUNCTION.\n    \"\"\"saves table to compressed tpz file.\n\n    Args:\n        path (Path): file destination.\n        compression_method: See zipfile compression methods. Defaults to ZIP_DEFLATED.\n        compression_level: See zipfile compression levels. Defaults to 1.\n        The default settings produce 80% compression at 10% slowdown.\n\n    The file format is as follows:\n    .tpz is a gzip archive with table metadata captured as table.yml\n    and the necessary set of pages saved as .npy files.\n\n    The zip contains table.yml which provides an overview of the data:\n    ```\n    --------------------------------------\n    %YAML 1.2                              yaml version\n    columns:                               start of columns section.\n        name: \u201c\u5217 1\u201d                       name of column 1.\n            pages: [p1b1, p1b2]            list of pages in column 1.\n        name: \u201c\u5217 2\u201d                       name of column 2\n            pages: [p2b1, p2b2]            list of pages in column 2.\n    ----------------------------------------\n    ```\n    \"\"\"\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path += \".tpz\"\n\n    # create yaml document\n    _page_counter = 0\n    d = {}\n    cols = {}\n    for name, col in self.columns.items():\n        type_check(col, Column)\n        cols[name] = {\"pages\": [p.path.name for p in col.pages]}\n        _page_counter += len(col.pages)\n    d[\"columns\"] = cols\n    yml = yaml.safe_dump(\n        d, sort_keys=False, allow_unicode=True, default_flow_style=None\n    )\n\n    _file_counter = 0\n    with zipfile.ZipFile(\n        path, \"w\", compression=compression_method, compresslevel=compression_level\n    ) as f:\n        log.debug(f\"writing .tpz to {path} with\\n{yml}\")\n        f.writestr(\"table.yml\", yml)\n        for name, col in self.columns.items():\n            for page in set(\n                col.pages\n            ):  # set of pages! remember t *= 1000 repeats t 1000x\n                with open(page.path, \"rb\", buffering=0) as raw_io:\n                    f.writestr(page.path.name, raw_io.read())\n                _file_counter += 1\n                log.debug(f\"adding Page {page.path}\")\n\n        _fields = len(self) * len(self.columns)\n        _avg = _fields // _page_counter\n        log.debug(\n            f\"Wrote {_fields:,} on {_page_counter:,} pages in {_file_counter} files: {_avg} fields/page\"\n        )\n
"},{"location":"reference/core/#tablite.core.Table.load","title":"tablite.core.Table.load(path, tqdm=_tqdm) classmethod","text":"

loads a table from .tpz file. See also Table.save for details on the file format.

PARAMETER DESCRIPTION path

source file

TYPE: Path

RETURNS DESCRIPTION Table

table in read-only mode.

Source code in tablite/base.py
@classmethod\ndef load(cls, path, tqdm=_tqdm):  # USER FUNCTION.\n    \"\"\"loads a table from .tpz file.\n    See also Table.save for details on the file format.\n\n    Args:\n        path (Path): source file\n\n    Returns:\n        Table: table in read-only mode.\n    \"\"\"\n    type_check(path, Path)\n    log.debug(f\"loading {path}\")\n    with zipfile.ZipFile(path, \"r\") as f:\n        yml = f.read(\"table.yml\")\n        metadata = yaml.safe_load(yml)\n        t = cls()\n\n        page_count = sum([len(c[\"pages\"]) for c in metadata[\"columns\"].values()])\n\n        with tqdm(\n            total=page_count,\n            desc=f\"loading '{path.name}' file\",\n            disable=Config.TQDM_DISABLE,\n        ) as pbar:\n            for name, d in metadata[\"columns\"].items():\n                column = Column(t.path)\n                for page in d[\"pages\"]:\n                    bytestream = io.BytesIO(f.read(page))\n                    data = np.load(bytestream, allow_pickle=True, fix_imports=False)\n                    column.extend(data)\n                    pbar.update(1)\n                t.columns[name] = column\n    update_access_time(path)\n    return t\n
"},{"location":"reference/core/#tablite.core.Table.copy","title":"tablite.core.Table.copy()","text":"Source code in tablite/base.py
def copy(self):\n    cls = type(self)\n    t = cls()\n    for name, column in self.columns.items():\n        new = Column(t.path)\n        new.pages = column.pages[:]\n        t.columns[name] = new\n    return t\n
"},{"location":"reference/core/#tablite.core.Table.__imul__","title":"tablite.core.Table.__imul__(other)","text":"

Repeats instance of table N times.

Like list: t = t * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"Repeats instance of table N times.\n\n    Like list: `t = t * N`\n\n    Args:\n        other (int): multiplier\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a table can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    for col in self.columns.values():\n        col *= other\n    return self\n
"},{"location":"reference/core/#tablite.core.Table.__mul__","title":"tablite.core.Table.__mul__(other)","text":"

Repeat table N times. Like list: new = old * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"Repeat table N times.\n    Like list: `new = old * N`\n\n    Args:\n        other (int): multiplier\n\n    Returns:\n        Table\n    \"\"\"\n    new = self.copy()\n    return new.__imul__(other)\n
"},{"location":"reference/core/#tablite.core.Table.__iadd__","title":"tablite.core.Table.__iadd__(other)","text":"

Concatenates tables with same column names.

Like list: table_1 += table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION None

self is updated.

Source code in tablite/base.py
def __iadd__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_1 += table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        None: self is updated.\n    \"\"\"\n    type_check(other, Table)\n    for name in self.columns.keys():\n        if name not in other.columns:\n            raise ValueError(f\"{name} not in other\")\n    for name in other.columns.keys():\n        if name not in self.columns:\n            raise ValueError(f\"{name} missing from self\")\n\n    for name, column in self.columns.items():\n        other_col = other.columns.get(name, None)\n        column.pages.extend(other_col.pages[:])\n    return self\n
"},{"location":"reference/core/#tablite.core.Table.__add__","title":"tablite.core.Table.__add__(other)","text":"

Concatenates tables with same column names.

Like list: table_3 = table_1 + table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __add__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_3 = table_1 + table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        Table\n    \"\"\"\n    type_check(other, Table)\n    cp = self.copy()\n    cp += other\n    return cp\n
"},{"location":"reference/core/#tablite.core.Table.add_rows","title":"tablite.core.Table.add_rows(*args, **kwargs)","text":"

its more efficient to add many rows at once.

if both args and kwargs, then args are added first, followed by kwargs.

supported cases:

>>> t = Table()\n>>> t.add_columns('row','A','B','C')\n>>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n>>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n>>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n>>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n>>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n>>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n>>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n>>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n>>> t.add_rows(\n    {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n    )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n>>> t.add_rows( *[\n    {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n    ])                                                  # (10) list of dicts as args\n>>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n
Source code in tablite/base.py
def add_rows(self, *args, **kwargs):\n    \"\"\"its more efficient to add many rows at once.\n\n    if both args and kwargs, then args are added first, followed by kwargs.\n\n    supported cases:\n    ```\n    >>> t = Table()\n    >>> t.add_columns('row','A','B','C')\n    >>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n    >>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n    >>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n    >>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n    >>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n    >>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n    >>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n    >>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n    >>> t.add_rows(\n        {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n        )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n    >>> t.add_rows( *[\n        {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n        ])                                                  # (10) list of dicts as args\n    >>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n    ```\n\n    \"\"\"\n    if not Table._add_row_slow_warning:\n        warnings.warn(\n            \"add_rows is slow. Consider using add_columns and then assigning values to the columns directly.\"\n        )\n        Table._add_row_slow_warning = True\n\n    if args:\n        if not all(isinstance(i, (list, tuple, dict)) for i in args):  # 1,4\n            args = [args]\n\n        if all(isinstance(i, (list, tuple, dict)) for i in args):  # 2,3,7,8\n            # 1. turn the data into columns:\n\n            d = {n: [] for n in self.columns}\n            for arg in args:\n                if len(arg) != len(self.columns):\n                    raise ValueError(\n                        f\"len({arg})== {len(arg)}, but there are {len(self.columns)} columns\"\n                    )\n\n                if isinstance(arg, dict):\n                    for k, v in arg.items():  # 7,8\n                        d[k].append(v)\n\n                elif isinstance(arg, (list, tuple)):  # 2,3\n                    for n, v in zip(self.columns, arg):\n                        d[n].append(v)\n\n                else:\n                    raise TypeError(f\"{arg}?\")\n            # 2. extend the columns\n            for n, values in d.items():\n                col = self.columns[n]\n                col.extend(list_to_np_array(values))\n\n    if kwargs:\n        if isinstance(kwargs, dict):\n            if all(isinstance(v, (list, tuple)) for v in kwargs.values()):\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(list_to_np_array(v))\n            else:\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(np.array([v]))\n        else:\n            raise ValueError(f\"format not recognised: {kwargs}\")\n\n    return\n
"},{"location":"reference/core/#tablite.core.Table.add_columns","title":"tablite.core.Table.add_columns(*names)","text":"

Adds column names to table.

Source code in tablite/base.py
def add_columns(self, *names):\n    \"\"\"Adds column names to table.\"\"\"\n    for name in names:\n        self.columns[name] = Column(self.path)\n
"},{"location":"reference/core/#tablite.core.Table.add_column","title":"tablite.core.Table.add_column(name, data=None)","text":"

verbose alias for table[name] = data, that checks if name already exists

PARAMETER DESCRIPTION name

column name

TYPE: str

data

values. Defaults to None.

TYPE: list,tuple) DEFAULT: None

RAISES DESCRIPTION TypeError

name isn't string

ValueError

name already exists

Source code in tablite/base.py
def add_column(self, name, data=None):\n    \"\"\"verbose alias for table[name] = data, that checks if name already exists\n\n    Args:\n        name (str): column name\n        data ((list,tuple), optional): values. Defaults to None.\n\n    Raises:\n        TypeError: name isn't string\n        ValueError: name already exists\n    \"\"\"\n    if not isinstance(name, str):\n        raise TypeError(\"expected name as string\")\n    if name in self.columns:\n        raise ValueError(f\"{name} already in {self.columns}\")\n    self.__setitem__(name, data)\n
"},{"location":"reference/core/#tablite.core.Table.stack","title":"tablite.core.Table.stack(other)","text":"

returns the joint stack of tables with overlapping column names. Example:

| Table A|  +  | Table B| = |  Table AB |\n| A| B| C|     | A| B| D|   | A| B| C| -|\n                            | A| B| -| D|\n
Source code in tablite/base.py
def stack(self, other):\n    \"\"\"\n    returns the joint stack of tables with overlapping column names.\n    Example:\n    ```\n    | Table A|  +  | Table B| = |  Table AB |\n    | A| B| C|     | A| B| D|   | A| B| C| -|\n                                | A| B| -| D|\n    ```\n    \"\"\"\n    if not isinstance(other, Table):\n        raise TypeError(f\"stack only works for Table, not {type(other)}\")\n\n    cp = self.copy()\n    for name, col2 in other.columns.items():\n        if name not in cp.columns:\n            cp[name] = [None] * len(self)\n        cp[name].pages.extend(col2.pages[:])\n\n    for name in self.columns:\n        if name not in other.columns:\n            if len(cp) > 0:\n                cp[name].extend(np.array([None] * len(other)))\n    return cp\n
"},{"location":"reference/core/#tablite.core.Table.types","title":"tablite.core.Table.types()","text":"

returns nested dict of data types in the form: {column name: {python type class: number of instances }, ... }

example:

>>> t.types()\n{\n    'A': {<class 'str'>: 7},\n    'B': {<class 'int'>: 7}\n}\n
Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns nested dict of data types in the form:\n    `{column name: {python type class: number of instances }, ... }`\n\n    example:\n    ```\n    >>> t.types()\n    {\n        'A': {<class 'str'>: 7},\n        'B': {<class 'int'>: 7}\n    }\n    ```\n    \"\"\"\n    d = {}\n    for name, col in self.columns.items():\n        assert isinstance(col, Column)\n        d[name] = col.types()\n    return d\n
"},{"location":"reference/core/#tablite.core.Table.display_dict","title":"tablite.core.Table.display_dict(slice_=None, blanks=None, dtype=False)","text":"

helper for creating dict for display.

PARAMETER DESCRIPTION slice_

python slice. Defaults to None.

TYPE: slice DEFAULT: None

blanks

fill value for None. Defaults to None.

TYPE: optional DEFAULT: None

dtype

Adds datatype to each column. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION TypeError

slice_ must be None or slice.

RETURNS DESCRIPTION dict

from Table.

Source code in tablite/base.py
def display_dict(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"helper for creating dict for display.\n\n    Args:\n        slice_ (slice, optional): python slice. Defaults to None.\n        blanks (optional): fill value for `None`. Defaults to None.\n        dtype (bool, optional): Adds datatype to each column. Defaults to False.\n\n    Raises:\n        TypeError: slice_ must be None or slice.\n\n    Returns:\n        dict: from Table.\n    \"\"\"\n    if not self.columns:\n        print(\"Empty Table\")\n        return\n\n    def datatype(col):  # PRIVATE\n        \"\"\"creates label for column datatype.\"\"\"\n        types = col.types()\n        if len(types) == 1:\n            dt, _ = types.popitem()\n            typ = dt.__name__\n        else:\n            typ = \"mixed\"\n        return typ\n\n    row_count_tags = [\"#\", \"~\", \"*\"]\n    cols = set(self.columns)\n    for n, tag in product(range(1, 6), row_count_tags):\n        if n * tag not in cols:\n            tag = n * tag\n            break\n\n    if not isinstance(slice_, (slice, type(None))):\n        raise TypeError(f\"slice_ must be None or slice, not {type(slice_)}\")\n    if isinstance(slice_, slice):\n        slc = slice_\n    if slice_ is None:\n        if len(self) <= 20:\n            slc = slice(0, 20, 1)\n        else:\n            slc = None\n\n    n = len(self)\n    if slc:  # either we want slc or we want everything.\n        row_no = list(range(*slc.indices(len(self))))\n        data = {tag: [f\"{i:,}\".rjust(2) for i in row_no]}\n        for name, col in self.columns.items():\n            data[name] = list(chain(iter(col), repeat(blanks, times=n - len(col))))[\n                slc\n            ]\n    else:\n        data = {}\n        j = int(math.ceil(math.log10(n)) / 3) + len(str(n))\n        row_no = (\n            [f\"{i:,}\".rjust(j) for i in range(7)]\n            + [\"...\"]\n            + [f\"{i:,}\".rjust(j) for i in range(n - 7, n)]\n        )\n        data = {tag: row_no}\n\n        for name, col in self.columns.items():\n            if len(col) == n:\n                row = col[:7].tolist() + [\"...\"] + col[-7:].tolist()\n            else:\n                empty = [blanks] * 7\n                head = (col[:7].tolist() + empty)[:7]\n                tail = (col[n - 7 :].tolist() + empty)[-7:]\n                row = head + [\"...\"] + tail\n            data[name] = row\n\n    if dtype:\n        for name, values in data.items():\n            if name in self.columns:\n                col = self.columns[name]\n                values.insert(0, datatype(col))\n            else:\n                values.insert(0, \"row\")\n\n    return data\n
"},{"location":"reference/core/#tablite.core.Table.to_ascii","title":"tablite.core.Table.to_ascii(slice_=None, blanks=None, dtype=False)","text":"

returns ascii view of table as string.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def to_ascii(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"returns ascii view of table as string.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n\n    def adjust(v, length):  # PRIVATE FUNCTION\n        \"\"\"whitespace justifies field values based on datatype\"\"\"\n        if v is None:\n            return str(blanks).ljust(length)\n        elif isinstance(v, str):\n            return v.ljust(length)\n        else:\n            return str(v).rjust(length)\n\n    if not self.columns:\n        return str(self)\n\n    d = {}\n    for name, values in self.display_dict(\n        slice_=slice_, blanks=blanks, dtype=dtype\n    ).items():\n        as_text = [str(v) for v in values] + [str(name)]\n        width = max(len(i) for i in as_text)\n        new_name = name.center(width, \" \")\n        if dtype:\n            values[0] = values[0].center(width, \" \")\n        d[new_name] = [adjust(v, width) for v in values]\n\n    rows = dict_to_rows(d)\n    s = []\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n    s.append(\"|\" + \"|\".join(rows[0]) + \"|\")  # column names\n    start = 1\n    if dtype:\n        s.append(\"|\" + \"|\".join(rows[1]) + \"|\")  # datatypes\n        start = 2\n\n    s.append(\"+\" + \"+\".join([\"-\" * len(n) for n in rows[0]]) + \"+\")\n    for row in rows[start:]:\n        s.append(\"|\" + \"|\".join(row) + \"|\")\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n\n    if len(set(len(c) for c in self.columns.values())) != 1:\n        warning = f\"Warning: Columns have different lengths. {blanks} is used as fill value.\"\n        s.append(warning)\n\n    return \"\\n\".join(s)\n
"},{"location":"reference/core/#tablite.core.Table.show","title":"tablite.core.Table.show(slice_=None, blanks=None, dtype=False)","text":"

prints ascii view of table.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def show(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"prints ascii view of table.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n    print(self.to_ascii(slice_=slice_, blanks=blanks, dtype=dtype))\n
"},{"location":"reference/core/#tablite.core.Table.to_dict","title":"tablite.core.Table.to_dict(columns=None, slice_=None)","text":"

columns: list of column names. Default is None == all columns. slice_: slice. Default is None == all rows.

returns: dict with columns as keys and lists of values.

Example:

>>> t.show()\n+===+===+===+\n| # | a | b |\n|row|int|int|\n+---+---+---+\n| 0 |  1|  3|\n| 1 |  2|  4|\n+===+===+===+\n>>> t.to_dict()\n{'a':[1,2], 'b':[3,4]}\n
Source code in tablite/base.py
def to_dict(self, columns=None, slice_=None):\n    \"\"\"\n    columns: list of column names. Default is None == all columns.\n    slice_: slice. Default is None == all rows.\n\n    returns: dict with columns as keys and lists of values.\n\n    Example:\n    ```\n    >>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  3|\n    | 1 |  2|  4|\n    +===+===+===+\n    >>> t.to_dict()\n    {'a':[1,2], 'b':[3,4]}\n    ```\n\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n    assert isinstance(slice_, slice)\n\n    if columns is None:\n        columns = list(self.columns.keys())\n    if not isinstance(columns, list):\n        raise TypeError(\"expected columns as list of strings\")\n\n    return {name: list(self.columns[name][slice_]) for name in columns}\n
"},{"location":"reference/core/#tablite.core.Table.as_json_serializable","title":"tablite.core.Table.as_json_serializable(row_count='row id', start_on=1, columns=None, slice_=None)","text":"

provides a JSON compatible format of the table.

PARAMETER DESCRIPTION row_count

Label for row counts. Defaults to \"row id\".

TYPE: str DEFAULT: 'row id'

start_on

row counts starts by default on 1.

TYPE: int DEFAULT: 1

columns

Column names. Defaults to None which returns all columns.

TYPE: list of str DEFAULT: None

slice_

selector. Defaults to None which returns [:]

TYPE: slice DEFAULT: None

RETURNS DESCRIPTION

JSON serializable dict: All python datatypes have been converted to JSON compliant data.

Source code in tablite/base.py
def as_json_serializable(\n    self, row_count=\"row id\", start_on=1, columns=None, slice_=None\n):\n    \"\"\"provides a JSON compatible format of the table.\n\n    Args:\n        row_count (str, optional): Label for row counts. Defaults to \"row id\".\n        start_on (int, optional): row counts starts by default on 1.\n        columns (list of str, optional): Column names.\n            Defaults to None which returns all columns.\n        slice_ (slice, optional): selector. Defaults to None which returns [:]\n\n    Returns:\n        JSON serializable dict: All python datatypes have been converted to JSON compliant data.\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n\n    assert isinstance(slice_, slice)\n    new = {\"columns\": {}, \"total_rows\": len(self)}\n    if row_count is not None:\n        new[\"columns\"][row_count] = [\n            i + start_on for i in range(*slice_.indices(len(self)))\n        ]\n\n    d = self.to_dict(columns, slice_=slice_)\n    for k, data in d.items():\n        new_k = unique_name(\n            k, new[\"columns\"]\n        )  # used to avoid overwriting the `row id` key.\n        new[\"columns\"][new_k] = [\n            DataTypes.to_json(v) for v in data\n        ]  # deal with non-json datatypes.\n    return new\n
"},{"location":"reference/core/#tablite.core.Table.index","title":"tablite.core.Table.index(*args)","text":"

param: *args: column names returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}

Examples:

>>> table6 = Table()\n>>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n>>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n
>>> table6.index('A')  # single key.\n{('Alice',): [0],\n ('Bob',): [1, 2],\n ('Ben',): [3, 5],\n ('Charlie',): [4],\n ('Albert',): [6]})\n
>>> table6.index('A', 'B')  # multiple keys.\n{('Alice', 'Alison'): [0],\n ('Bob', 'Marley'): [1],\n ('Bob', 'Dylan'): [2],\n ('Ben', 'Affleck'): [3],\n ('Charlie', 'Hepburn'): [4],\n ('Ben', 'Barnes'): [5],\n ('Albert', 'Einstein'): [6]})\n
Source code in tablite/base.py
def index(self, *args):\n    \"\"\"\n    param: *args: column names\n    returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}\n\n    Examples:\n        ```\n        >>> table6 = Table()\n        >>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n        >>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n        ```\n\n        ```\n        >>> table6.index('A')  # single key.\n        {('Alice',): [0],\n         ('Bob',): [1, 2],\n         ('Ben',): [3, 5],\n         ('Charlie',): [4],\n         ('Albert',): [6]})\n        ```\n\n        ```\n        >>> table6.index('A', 'B')  # multiple keys.\n        {('Alice', 'Alison'): [0],\n         ('Bob', 'Marley'): [1],\n         ('Bob', 'Dylan'): [2],\n         ('Ben', 'Affleck'): [3],\n         ('Charlie', 'Hepburn'): [4],\n         ('Ben', 'Barnes'): [5],\n         ('Albert', 'Einstein'): [6]})\n        ```\n\n    \"\"\"\n    idx = defaultdict(list)\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in enumerate(zip(*iterators)):\n        key = tuple(numpy_to_python(k) for k in key)\n        idx[key].append(ix)\n    return idx\n
"},{"location":"reference/core/#tablite.core.Table.unique_index","title":"tablite.core.Table.unique_index(*args, tqdm=_tqdm)","text":"

generates the index of unique rows given a list of column names

PARAMETER DESCRIPTION *args

columns names

TYPE: any DEFAULT: ()

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION

np.array(int64): indices of unique records.

Source code in tablite/base.py
def unique_index(self, *args, tqdm=_tqdm):\n    \"\"\"generates the index of unique rows given a list of column names\n\n    Args:\n        *args (any): columns names\n        tqdm (tqdm, optional): Defaults to _tqdm.\n\n    Returns:\n        np.array(int64): indices of unique records.\n    \"\"\"\n    if not args:\n        raise ValueError(\"*args (column names) is required\")\n    seen = set()\n    unique = set()\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in tqdm(enumerate(zip(*iterators)), disable=Config.TQDM_DISABLE):\n        key_hash = hash(tuple(numpy_to_python(k) for k in key))\n        if key_hash in seen:\n            continue\n        else:\n            seen.add(key_hash)\n            unique.add(ix)\n    return np.array(sorted(unique))\n
"},{"location":"reference/core/#tablite.core.Table.from_file","title":"tablite.core.Table.from_file(path, columns=None, first_row_has_headers=True, header_row_index=0, encoding=None, start=0, limit=sys.maxsize, sheet=None, guess_datatypes=True, newline='\\n', text_qualifier=None, delimiter=None, strip_leading_and_tailing_whitespace=True, text_escape_openings='', text_escape_closures='', tqdm=_tqdm) -> Table classmethod","text":"
    reads path and imports 1 or more tables\n\n    REQUIRED\n    --------\n    path: pathlib.Path or str\n        selection of filereader uses path.suffix.\n        See `filereaders`.\n\n    OPTIONAL\n    --------\n    columns:\n        None: (default) All columns will be imported.\n        List: only column names from list will be imported (if present in file)\n              e.g. ['A', 'B', 'C', 'D']\n\n              datatype is detected using Datatypes.guess(...)\n              You can try it out with:\n              >> from tablite.datatypes import DataTypes\n              >> DataTypes.guess(['001','100'])\n              [1,100]\n\n              if the format cannot be achieved the read type is kept.\n        Excess column names are ignored.\n\n        HINT: To get the head of file use:\n        >>> from tablite.tools import head\n        >>> head = head(path)\n\n    first_row_has_headers: boolean\n        True: (default) first row is used as column names.\n        False: integers are used as column names.\n\n    encoding: str. Defaults to None (autodetect using n bytes).\n        n is declared in filereader_utils as ENCODING_GUESS_BYTES\n\n    start: the first line to be read (default: 0)\n\n    limit: the number of lines to be read from start (default sys.maxint ~ 2**63)\n\n    OPTIONAL FOR EXCEL AND ODS READERS\n    ----------------------------------\n\n    sheet: sheet name to import  (applicable to excel- and ods-reader only)\n        e.g. 'sheet_1'\n        sheets not found excess names are ignored.\n\n    OPTIONAL FOR TEXT READERS\n    -------------------------\n    guess_datatype: bool\n        True: (default) datatypes are guessed using DataTypes.guess(...)\n        False: all data is imported as strings.\n\n    newline: newline character (applicable to text_reader only)\n        str: '\n

' (default) or ' '

    text_qualifier: character (applicable to text_reader only)\n        None: No text qualifier is used.\n        str: \" or '\n\n    delimiter: character (applicable to text_reader only)\n        None: file suffix is used to determine field delimiter:\n            .txt: \"|\"\n            .csv: \",\",\n            .ssv: \";\"\n            .tsv: \" \" (tab)\n\n    strip_leading_and_tailing_whitespace: bool:\n        True: default\n\n    text_escape_openings: (applicable to text_reader only)\n        None: default\n        str: list of characters such as ([{\n\n    text_escape_closures: (applicable to text_reader only)\n        None: default\n        str: list of characters such as }])\n
Source code in tablite/core.py
@classmethod\ndef from_file(\n    cls,\n    path,\n    columns=None,\n    first_row_has_headers=True,\n    header_row_index=0,\n    encoding=None,\n    start=0,\n    limit=sys.maxsize,\n    sheet=None,\n    guess_datatypes=True,\n    newline=\"\\n\",\n    text_qualifier=None,\n    delimiter=None,\n    strip_leading_and_tailing_whitespace=True,\n    text_escape_openings=\"\",\n    text_escape_closures=\"\",\n    tqdm=_tqdm,\n) -> \"Table\":\n    \"\"\"\n    reads path and imports 1 or more tables\n\n    REQUIRED\n    --------\n    path: pathlib.Path or str\n        selection of filereader uses path.suffix.\n        See `filereaders`.\n\n    OPTIONAL\n    --------\n    columns:\n        None: (default) All columns will be imported.\n        List: only column names from list will be imported (if present in file)\n              e.g. ['A', 'B', 'C', 'D']\n\n              datatype is detected using Datatypes.guess(...)\n              You can try it out with:\n              >> from tablite.datatypes import DataTypes\n              >> DataTypes.guess(['001','100'])\n              [1,100]\n\n              if the format cannot be achieved the read type is kept.\n        Excess column names are ignored.\n\n        HINT: To get the head of file use:\n        >>> from tablite.tools import head\n        >>> head = head(path)\n\n    first_row_has_headers: boolean\n        True: (default) first row is used as column names.\n        False: integers are used as column names.\n\n    encoding: str. Defaults to None (autodetect using n bytes).\n        n is declared in filereader_utils as ENCODING_GUESS_BYTES\n\n    start: the first line to be read (default: 0)\n\n    limit: the number of lines to be read from start (default sys.maxint ~ 2**63)\n\n    OPTIONAL FOR EXCEL AND ODS READERS\n    ----------------------------------\n\n    sheet: sheet name to import  (applicable to excel- and ods-reader only)\n        e.g. 'sheet_1'\n        sheets not found excess names are ignored.\n\n    OPTIONAL FOR TEXT READERS\n    -------------------------\n    guess_datatype: bool\n        True: (default) datatypes are guessed using DataTypes.guess(...)\n        False: all data is imported as strings.\n\n    newline: newline character (applicable to text_reader only)\n        str: '\\n' (default) or '\\r\\n'\n\n    text_qualifier: character (applicable to text_reader only)\n        None: No text qualifier is used.\n        str: \" or '\n\n    delimiter: character (applicable to text_reader only)\n        None: file suffix is used to determine field delimiter:\n            .txt: \"|\"\n            .csv: \",\",\n            .ssv: \";\"\n            .tsv: \"\\t\" (tab)\n\n    strip_leading_and_tailing_whitespace: bool:\n        True: default\n\n    text_escape_openings: (applicable to text_reader only)\n        None: default\n        str: list of characters such as ([{\n\n    text_escape_closures: (applicable to text_reader only)\n        None: default\n        str: list of characters such as }])\n\n    \"\"\"\n    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n\n    if not path.exists():\n        raise FileNotFoundError(f\"file not found: {path}\")\n\n    if not isinstance(start, int) or not 0 <= start <= sys.maxsize:\n        raise ValueError(f\"start {start} not in range(0,{sys.maxsize})\")\n\n    if not isinstance(limit, int) or not 0 < limit <= sys.maxsize:\n        raise ValueError(f\"limit {limit} not in range(0,{sys.maxsize})\")\n\n    if not isinstance(first_row_has_headers, bool):\n        raise TypeError(\"first_row_has_headers is not bool\")\n\n    import_as = path.suffix\n    if import_as.startswith(\".\"):\n        import_as = import_as[1:]\n\n    reader = import_utils.file_readers.get(import_as, None)\n    if reader is None:\n        raise ValueError(f\"{import_as} is not in supported format: {import_utils.valid_readers}\")\n\n    additional_configs = {\"tqdm\": tqdm}\n    if reader == import_utils.text_reader:\n        # here we inject tqdm, if tqdm is not provided, use generic iterator\n        # fmt:off\n        config = (path, columns, first_row_has_headers, header_row_index, encoding, start, limit, newline,\n                  guess_datatypes, text_qualifier, strip_leading_and_tailing_whitespace,\n                  delimiter, text_escape_openings, text_escape_closures)\n        # fmt:on\n\n    elif reader == import_utils.from_html:\n        config = (path,)\n    elif reader == import_utils.from_hdf5:\n        config = (path,)\n\n    elif reader == import_utils.excel_reader:\n        # config = path, first_row_has_headers, sheet, columns, start, limit\n        config = (\n            path,\n            first_row_has_headers,\n            header_row_index,\n            sheet,\n            columns,\n            start,\n            limit,\n        )  # if file length changes - re-import.\n\n    if reader == import_utils.ods_reader:\n        # path, first_row_has_headers=True, sheet=None, columns=None, start=0, limit=sys.maxsize,\n        config = (\n            str(path),\n            first_row_has_headers,\n            header_row_index,\n            sheet,\n            columns,\n            start,\n            limit,\n        )  # if file length changes - re-import.\n\n    # At this point the import config seems valid.\n    # Now we check if the file already has been imported.\n\n    # publish the settings\n    return reader(cls, *config, **additional_configs)\n
"},{"location":"reference/core/#tablite.core.Table.from_pandas","title":"tablite.core.Table.from_pandas(df) classmethod","text":"

Creates Table using pd.to_dict('list')

similar to:

>>> import pandas as pd\n>>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n>>> df\n    a  b\n    0  1  4\n    1  2  5\n    2  3  6\n>>> df.to_dict('list')\n{'a': [1, 2, 3], 'b': [4, 5, 6]}\n>>> t = Table.from_dict(df.to_dict('list))\n>>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  4|\n    | 1 |  2|  5|\n    | 2 |  3|  6|\n    +===+===+===+\n
Source code in tablite/core.py
@classmethod\ndef from_pandas(cls, df):\n    \"\"\"\n    Creates Table using pd.to_dict('list')\n\n    similar to:\n    ```\n    >>> import pandas as pd\n    >>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n    >>> df\n        a  b\n        0  1  4\n        1  2  5\n        2  3  6\n    >>> df.to_dict('list')\n    {'a': [1, 2, 3], 'b': [4, 5, 6]}\n    >>> t = Table.from_dict(df.to_dict('list))\n    >>> t.show()\n        +===+===+===+\n        | # | a | b |\n        |row|int|int|\n        +---+---+---+\n        | 0 |  1|  4|\n        | 1 |  2|  5|\n        | 2 |  3|  6|\n        +===+===+===+\n    ```\n    \"\"\"\n    return import_utils.from_pandas(cls, df)\n
"},{"location":"reference/core/#tablite.core.Table.from_hdf5","title":"tablite.core.Table.from_hdf5(path) classmethod","text":"

imports an exported hdf5 table.

Source code in tablite/core.py
@classmethod\ndef from_hdf5(cls, path):\n    \"\"\"\n    imports an exported hdf5 table.\n    \"\"\"\n    return import_utils.from_hdf5(cls, path)\n
"},{"location":"reference/core/#tablite.core.Table.from_json","title":"tablite.core.Table.from_json(jsn) classmethod","text":"

Imports table exported using .to_json

Source code in tablite/core.py
@classmethod\ndef from_json(cls, jsn):\n    \"\"\"\n    Imports table exported using .to_json\n    \"\"\"\n    return import_utils.from_json(cls, jsn)\n
"},{"location":"reference/core/#tablite.core.Table.to_hdf5","title":"tablite.core.Table.to_hdf5(path)","text":"

creates a copy of the table as hdf5

Source code in tablite/core.py
def to_hdf5(self, path):\n    \"\"\"\n    creates a copy of the table as hdf5\n    \"\"\"\n    export_utils.to_hdf5(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_pandas","title":"tablite.core.Table.to_pandas()","text":"

returns pandas.DataFrame

Source code in tablite/core.py
def to_pandas(self):\n    \"\"\"\n    returns pandas.DataFrame\n    \"\"\"\n    return export_utils.to_pandas(self)\n
"},{"location":"reference/core/#tablite.core.Table.to_sql","title":"tablite.core.Table.to_sql(name)","text":"

generates ANSI-92 compliant SQL.

Source code in tablite/core.py
def to_sql(self, name):\n    \"\"\"\n    generates ANSI-92 compliant SQL.\n    \"\"\"\n    return export_utils.to_sql(self, name)  # remove after update to test suite.\n
"},{"location":"reference/core/#tablite.core.Table.to_json","title":"tablite.core.Table.to_json()","text":"

returns JSON

Source code in tablite/core.py
def to_json(self):\n    \"\"\"\n    returns JSON\n    \"\"\"\n    return export_utils.to_json(self)\n
"},{"location":"reference/core/#tablite.core.Table.to_xlsx","title":"tablite.core.Table.to_xlsx(path)","text":"

exports table to path

Source code in tablite/core.py
def to_xlsx(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".xlsx\")\n    export_utils.excel_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_ods","title":"tablite.core.Table.to_ods(path)","text":"

exports table to path

Source code in tablite/core.py
def to_ods(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".ods\")\n    export_utils.excel_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_csv","title":"tablite.core.Table.to_csv(path)","text":"

exports table to path

Source code in tablite/core.py
def to_csv(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".csv\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_tsv","title":"tablite.core.Table.to_tsv(path)","text":"

exports table to path

Source code in tablite/core.py
def to_tsv(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".tsv\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_text","title":"tablite.core.Table.to_text(path)","text":"

exports table to path

Source code in tablite/core.py
def to_text(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".txt\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_html","title":"tablite.core.Table.to_html(path)","text":"

exports table to path

Source code in tablite/core.py
def to_html(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".html\")\n    export_utils.to_html(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.expression","title":"tablite.core.Table.expression(expression)","text":"

filters based on an expression, such as:

\"all((A==B, C!=4, 200<D))\"\n

which is interpreted using python's compiler to:

def _f(A,B,C,D):\n    return all((A==B, C!=4, 200<D))\n
Source code in tablite/core.py
def expression(self, expression):\n    \"\"\"\n    filters based on an expression, such as:\n\n        \"all((A==B, C!=4, 200<D))\"\n\n    which is interpreted using python's compiler to:\n\n        def _f(A,B,C,D):\n            return all((A==B, C!=4, 200<D))\n    \"\"\"\n    return redux._filter_using_expression(self, expression)\n
"},{"location":"reference/core/#tablite.core.Table.filter","title":"tablite.core.Table.filter(expressions, filter_type='all', tqdm=_tqdm)","text":"

enables filtering across columns for multiple criteria.

expressions:

str: Expression that can be compiled and executed row by row.\n    exampLe: \"all((A==B and C!=4 and 200<D))\"\n\nlist of dicts: (example):\n\n    L = [\n        {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n        {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n        {'value1': 200, 'criteria': \"<\", column2: 'D' }\n    ]\n\naccepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n

filter_type: 'all' or 'any'

Source code in tablite/core.py
def filter(self, expressions, filter_type=\"all\", tqdm=_tqdm):\n    \"\"\"\n    enables filtering across columns for multiple criteria.\n\n    expressions:\n\n        str: Expression that can be compiled and executed row by row.\n            exampLe: \"all((A==B and C!=4 and 200<D))\"\n\n        list of dicts: (example):\n\n            L = [\n                {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n                {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n                {'value1': 200, 'criteria': \"<\", column2: 'D' }\n            ]\n\n        accepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n\n    filter_type: 'all' or 'any'\n    \"\"\"\n    return redux.filter(self, expressions, filter_type, tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.sort_index","title":"tablite.core.Table.sort_index(sort_mode='excel', tqdm=_tqdm, pbar=None, **kwargs)","text":"

helper for methods sort and is_sorted

param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default) param: **kwargs: sort criteria. See Table.sort()

Source code in tablite/core.py
def sort_index(self, sort_mode=\"excel\", tqdm=_tqdm, pbar=None, **kwargs):\n    \"\"\"\n    helper for methods `sort` and `is_sorted`\n\n    param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default)\n    param: **kwargs: sort criteria. See Table.sort()\n    \"\"\"\n    return sortation.sort_index(self, sort_mode, tqdm=tqdm, pbar=pbar, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.reindex","title":"tablite.core.Table.reindex(index)","text":"

index: list of integers that declare sort order.

Examples:

Table:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6]\nresult: ['b','d','f','h']\n\nTable:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6,1,3,5,7]\nresult: ['a','c','e','g','b','d','f','h']\n
Source code in tablite/core.py
def reindex(self, index):\n    \"\"\"\n    index: list of integers that declare sort order.\n\n    Examples:\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6]\n        result: ['b','d','f','h']\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6,1,3,5,7]\n        result: ['a','c','e','g','b','d','f','h']\n\n    \"\"\"\n    if isinstance(index, list):\n        index = np.array(index)\n    return _reindex.reindex(self, index)\n
"},{"location":"reference/core/#tablite.core.Table.drop_duplicates","title":"tablite.core.Table.drop_duplicates(*args)","text":"

removes duplicate rows based on column names

args: (optional) column_names if no args, all columns are used.

Source code in tablite/core.py
def drop_duplicates(self, *args):\n    \"\"\"\n    removes duplicate rows based on column names\n\n    args: (optional) column_names\n    if no args, all columns are used.\n    \"\"\"\n    if not args:\n        args = self.columns\n    index = self.unique_index(*args)\n    return self.reindex(index)\n
"},{"location":"reference/core/#tablite.core.Table.sort","title":"tablite.core.Table.sort(mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

Perform multi-pass sorting with precedence given order of column names.

PARAMETER DESCRIPTION mapping

keys as columns, values as boolean for 'reverse'

TYPE: dict

sort_mode

str: \"alphanumeric\", \"unix\", or, \"excel\"

DEFAULT: 'excel'

RETURNS DESCRIPTION None

Table.sort is sorted inplace

Examples: Table.sort(mappinp={A':False}) means sort by 'A' in ascending order. Table.sort(mapping={'A':True, 'B':False}) means sort 'A' in descending order, then (2nd priority) sort B in ascending order.

Source code in tablite/core.py
def sort(self, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"Perform multi-pass sorting with precedence given order of column names.\n\n    Args:\n        mapping (dict): keys as columns,\n                        values as boolean for 'reverse'\n        sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\"\n\n    Returns:\n        None: Table.sort is sorted inplace\n\n    Examples:\n    Table.sort(mappinp={A':False}) means sort by 'A' in ascending order.\n    Table.sort(mapping={'A':True, 'B':False}) means sort 'A' in descending order, then (2nd priority)\n    sort B in ascending order.\n    \"\"\"\n    new = sortation.sort(self, mapping, sort_mode, tqdm=tqdm, pbar=pbar)\n    self.columns = new.columns\n
"},{"location":"reference/core/#tablite.core.Table.sorted","title":"tablite.core.Table.sorted(mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

See sort. Sorted returns a new table in contrast to \"sort\", which is in-place.

RETURNS DESCRIPTION

Table.

Source code in tablite/core.py
def sorted(self, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"See sort.\n    Sorted returns a new table in contrast to \"sort\", which is in-place.\n\n    Returns:\n        Table.\n    \"\"\"\n    return sortation.sort(self, mapping, sort_mode, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.is_sorted","title":"tablite.core.Table.is_sorted(mapping, sort_mode='excel')","text":"

Performs multi-pass sorting check with precedence given order of column names. **kwargs: optional: sort criteria. See Table.sort() :return bool

Source code in tablite/core.py
def is_sorted(self, mapping, sort_mode=\"excel\"):\n    \"\"\"Performs multi-pass sorting check with precedence given order of column names.\n    **kwargs: optional: sort criteria. See Table.sort()\n    :return bool\n    \"\"\"\n    return sortation.is_sorted(self, mapping, sort_mode)\n
"},{"location":"reference/core/#tablite.core.Table.any","title":"tablite.core.Table.any(**kwargs)","text":"

returns Table for rows where ANY kwargs match :param kwargs: dictionary with headers and values / boolean callable

Source code in tablite/core.py
def any(self, **kwargs):\n    \"\"\"\n    returns Table for rows where ANY kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n    \"\"\"\n    return redux.filter_any(self, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.all","title":"tablite.core.Table.all(**kwargs)","text":"

returns Table for rows where ALL kwargs match :param kwargs: dictionary with headers and values / boolean callable

Examples:

t = Table()\nt['a'] = [1,2,3,4]\nt['b'] = [10,20,30,40]\n\ndef f(x):\n    return x == 4\ndef g(x):\n    return x < 20\n\nt2 = t.any( **{\"a\":f, \"b\":g})\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\nt2 = t.any(a=f,b=g)\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\ndef h(x):\n    return x>=2\n\ndef i(x):\n    return x<=30\n\nt2 = t.all(a=h,b=i)\nassert [r for r in t2.rows] == [[2,20], [3, 30]]\n
Source code in tablite/core.py
def all(self, **kwargs):\n    \"\"\"\n    returns Table for rows where ALL kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n\n    Examples:\n\n        t = Table()\n        t['a'] = [1,2,3,4]\n        t['b'] = [10,20,30,40]\n\n        def f(x):\n            return x == 4\n        def g(x):\n            return x < 20\n\n        t2 = t.any( **{\"a\":f, \"b\":g})\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        t2 = t.any(a=f,b=g)\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        def h(x):\n            return x>=2\n\n        def i(x):\n            return x<=30\n\n        t2 = t.all(a=h,b=i)\n        assert [r for r in t2.rows] == [[2,20], [3, 30]]\n\n\n    \"\"\"\n    return redux.filter_all(self, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.drop","title":"tablite.core.Table.drop(*args)","text":"

removes all rows where args are present.

Exmaple:

t = Table() t['A'] = [1,2,3,None] t['B'] = [None,2,3,4] t2 = t.drop(None) t2'A', t2'B' ([2,3], [2,3])

Source code in tablite/core.py
def drop(self, *args):\n    \"\"\"\n    removes all rows where args are present.\n\n    Exmaple:\n    >>> t = Table()\n    >>> t['A'] = [1,2,3,None]\n    >>> t['B'] = [None,2,3,4]\n    >>> t2 = t.drop(None)\n    >>> t2['A'][:], t2['B'][:]\n    ([2,3], [2,3])\n\n    \"\"\"\n    if not args:\n        raise ValueError(\"What to drop? None? np.nan? \")\n    return redux.drop(self, *args)\n
"},{"location":"reference/core/#tablite.core.Table.replace","title":"tablite.core.Table.replace(mapping, columns=None, tqdm=_tqdm, pbar=None)","text":"

replaces all mapped keys with values from named columns

PARAMETER DESCRIPTION mapping

keys are targets for replacement, values are replacements.

TYPE: dict

columns

target columns. Defaults to None (all columns)

TYPE: list or str DEFAULT: None

RAISES DESCRIPTION ValueError

description

Source code in tablite/core.py
def replace(self, mapping, columns=None, tqdm=_tqdm, pbar=None):\n    \"\"\"replaces all mapped keys with values from named columns\n\n    Args:\n        mapping (dict): keys are targets for replacement,\n                        values are replacements.\n        columns (list or str, optional): target columns.\n            Defaults to None (all columns)\n\n    Raises:\n        ValueError: _description_\n    \"\"\"\n    if columns is None:\n        columns = list(self.columns)\n    if not isinstance(columns, list) and columns in self.columns:\n        columns = [columns]\n    type_check(columns, list)\n    for n in columns:\n        if n not in self.columns:\n            raise ValueError(f\"column not found: {n}\")\n\n    if pbar is None:\n        total = len(columns)\n        pbar = tqdm(total=total, desc=\"replace\", disable=Config.TQDM_DISABLE)\n\n    for name in columns:\n        col = self.columns[name]\n        col.replace(mapping)\n        pbar.update(1)\n
"},{"location":"reference/core/#tablite.core.Table.groupby","title":"tablite.core.Table.groupby(keys, functions, tqdm=_tqdm, pbar=None)","text":"

keys: column names for grouping. functions: [optional] list of column names and group functions (See GroupyBy class) returns: table

Example:

t = Table()\nt.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\nt.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\nt.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n\nt.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\ng = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\ng.show()\n+===+===+===+======+\n| # | A | C |Sum(B)|\n|row|int|int| int  |\n+---+---+---+------+\n|0  |  1|  6|     2|\n|1  |  1|  5|     4|\n|2  |  2|  4|     6|\n|3  |  2|  3|     8|\n|4  |  3|  2|    10|\n|5  |  3|  1|    12|\n+===+===+===+======+\n

Cheat sheet:

list of unique values

>>> g1 = t.groupby(keys=['A'], functions=[])\n>>> g1['A'][:]\n[1,2,3]\n

alternatively:

t['A'].unique() [1,2,3]

list of unique values, grouped by longest combination.

>>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n>>> g2['A'][:], g2['B'][:]\n([1,1,2,2,3,3], [1,2,3,4,5,6])\n

alternatively:

>>> list(zip(*t.index('A', 'B').keys()))\n[(1,1,2,2,3,3) (1,2,3,4,5,6)]\n

A key (unique values) and count hereof.

>>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n>>> g3['A'][:], g3['Count(A)'][:]\n([1,2,3], [4,4,4])\n

alternatively:

>>> t['A'].histogram()\n([1,2,3], [4,4,4])\n

for more exmaples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py

Source code in tablite/core.py
def groupby(self, keys, functions, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    keys: column names for grouping.\n    functions: [optional] list of column names and group functions (See GroupyBy class)\n    returns: table\n\n    Example:\n    ```\n    t = Table()\n    t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n    t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n    t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n\n    t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n    g.show()\n    +===+===+===+======+\n    | # | A | C |Sum(B)|\n    |row|int|int| int  |\n    +---+---+---+------+\n    |0  |  1|  6|     2|\n    |1  |  1|  5|     4|\n    |2  |  2|  4|     6|\n    |3  |  2|  3|     8|\n    |4  |  3|  2|    10|\n    |5  |  3|  1|    12|\n    +===+===+===+======+\n    ```\n    Cheat sheet:\n\n    list of unique values\n    ```\n    >>> g1 = t.groupby(keys=['A'], functions=[])\n    >>> g1['A'][:]\n    [1,2,3]\n    ```\n    alternatively:\n    >>> t['A'].unique()\n    [1,2,3]\n\n    list of unique values, grouped by longest combination.\n    ```\n    >>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n    >>> g2['A'][:], g2['B'][:]\n    ([1,1,2,2,3,3], [1,2,3,4,5,6])\n    ```\n    alternatively:\n    ```\n    >>> list(zip(*t.index('A', 'B').keys()))\n    [(1,1,2,2,3,3) (1,2,3,4,5,6)]\n    ```\n    A key (unique values) and count hereof.\n    ```\n    >>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n    >>> g3['A'][:], g3['Count(A)'][:]\n    ([1,2,3], [4,4,4])\n    ```\n    alternatively:\n    ```\n    >>> t['A'].histogram()\n    ([1,2,3], [4,4,4])\n    ```\n    for more exmaples see:\n        https://github.com/root-11/tablite/blob/master/tests/test_groupby.py\n\n    \"\"\"\n    return groupbys.groupby(self, keys, functions, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.pivot","title":"tablite.core.Table.pivot(rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None)","text":"

param: rows: column names to keep as rows param: columns: column names to keep as columns param: functions: aggregation functions from the Groupby class as

example:

t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\nt2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\nt2.show()\n+===+===+========+=====+=====+=====+\n| # | C |function|(A=1)|(A=2)|(A=3)|\n|row|int|  str   |mixed|mixed|mixed|\n+---+---+--------+-----+-----+-----+\n|0  |  6|Sum(B)  |    2|None |None |\n|1  |  5|Sum(B)  |    4|None |None |\n|2  |  4|Sum(B)  |None |    6|None |\n|3  |  3|Sum(B)  |None |    8|None |\n|4  |  2|Sum(B)  |None |None |   10|\n|5  |  1|Sum(B)  |None |None |   12|\n+===+===+========+=====+=====+=====+\n
Source code in tablite/core.py
def pivot(self, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    param: rows: column names to keep as rows\n    param: columns: column names to keep as columns\n    param: functions: aggregation functions from the Groupby class as\n\n    example:\n    ```\n    t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n    t2.show()\n    +===+===+========+=====+=====+=====+\n    | # | C |function|(A=1)|(A=2)|(A=3)|\n    |row|int|  str   |mixed|mixed|mixed|\n    +---+---+--------+-----+-----+-----+\n    |0  |  6|Sum(B)  |    2|None |None |\n    |1  |  5|Sum(B)  |    4|None |None |\n    |2  |  4|Sum(B)  |None |    6|None |\n    |3  |  3|Sum(B)  |None |    8|None |\n    |4  |  2|Sum(B)  |None |None |   10|\n    |5  |  1|Sum(B)  |None |None |   12|\n    +===+===+========+=====+=====+=====+\n    ```\n    \"\"\"\n    return pivots.pivot(self, rows, columns, functions, values_as_rows, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.merge","title":"tablite.core.Table.merge(left, right, new, criteria)","text":"

takes from LEFT where criteria is True else RIGHT. :param: T: Table :param: criteria: np.array(bool): if True take left column else take right column :param left: (str) column name :param right: (str) column name :param new: (str) new name

:returns: T

Example:

>>> c.show()\n+==+====+====+====+====+\n| #| A  | B  | C  | D  |\n+--+----+----+----+----+\n| 0|   1|  10|   1|  11|\n| 1|   2|  20|   2|  12|\n| 2|   3|None|   3|  13|\n| 3|None|  40|None|None|\n| 4|   5|  50|None|None|\n| 5|None|None|   6|  16|\n| 6|None|None|   7|  17|\n+==+====+====+====+====+\n\n>>> c.merge(\"A\", \"C\", new=\"E\", criteria=[v != None for v in c['A']])\n>>> c.show()\n+==+====+====+====+\n| #| B  | D  | E  |\n+--+----+----+----+\n| 0|  10|  11|   1|\n| 1|  20|  12|   2|\n| 2|None|  13|   3|\n| 3|  40|None|None|\n| 4|  50|None|   5|\n| 5|None|  16|   6|\n| 6|None|  17|   7|\n+==+====+====+====+\n
Source code in tablite/core.py
def merge(self, left, right, new, criteria):\n    \"\"\" takes from LEFT where criteria is True else RIGHT.\n    :param: T: Table\n    :param: criteria: np.array(bool): \n            if True take left column\n            else take right column\n    :param left: (str) column name\n    :param right: (str) column name\n    :param new: (str) new name\n\n    :returns: T\n\n    Example:\n    ```\n    >>> c.show()\n    +==+====+====+====+====+\n    | #| A  | B  | C  | D  |\n    +--+----+----+----+----+\n    | 0|   1|  10|   1|  11|\n    | 1|   2|  20|   2|  12|\n    | 2|   3|None|   3|  13|\n    | 3|None|  40|None|None|\n    | 4|   5|  50|None|None|\n    | 5|None|None|   6|  16|\n    | 6|None|None|   7|  17|\n    +==+====+====+====+====+\n\n    >>> c.merge(\"A\", \"C\", new=\"E\", criteria=[v != None for v in c['A']])\n    >>> c.show()\n    +==+====+====+====+\n    | #| B  | D  | E  |\n    +--+----+----+----+\n    | 0|  10|  11|   1|\n    | 1|  20|  12|   2|\n    | 2|None|  13|   3|\n    | 3|  40|None|None|\n    | 4|  50|None|   5|\n    | 5|None|  16|   6|\n    | 6|None|  17|   7|\n    +==+====+====+====+\n    ```\n    \"\"\"\n    return merge.where(self, criteria,left,right,new)\n
"},{"location":"reference/core/#tablite.core.Table.column_select","title":"tablite.core.Table.column_select(cols, tqdm=_tqdm, TaskManager=_TaskManager)","text":"

type-casts columns from a given table to specified type(s)

cols

list of dicts: (example):

cols = [\n    {'column':'A', 'type': 'bool'},\n    {'column':'B', 'type': 'int', 'allow_empty': True},\n    {'column':'B', 'type': 'float', 'allow_empty': False, 'rename': 'C'},\n]\n

'column' : column name of the input table that we want to type-cast 'type' : type that we want to type-cast the specified column to 'allow_empty': should we allow empty values (None, str('')) through (Default: False) 'rename' : new name of the column, if None will keep the original name, in case of duplicates suffix will be added (Default: None)

supported types: 'bool', 'int', 'float', 'str', 'date', 'time', 'datetime'

if any of the columns is rejected, entire row is rejected

tqdm: progressbar constructor TaskManager: TaskManager constructor

(TABLE, TABLE) DESCRIPTION

first table contains the rows that were successfully cast to desired types

second table contains rows that failed to cast + rejection reason

Source code in tablite/core.py
def column_select(self, cols, tqdm=_tqdm, TaskManager=_TaskManager):\n    \"\"\"\n    type-casts columns from a given table to specified type(s)\n\n    cols:\n        list of dicts: (example):\n\n            cols = [\n                {'column':'A', 'type': 'bool'},\n                {'column':'B', 'type': 'int', 'allow_empty': True},\n                {'column':'B', 'type': 'float', 'allow_empty': False, 'rename': 'C'},\n            ]\n\n        'column'     : column name of the input table that we want to type-cast\n        'type'       : type that we want to type-cast the specified column to\n        'allow_empty': should we allow empty values (None, str('')) through (Default: False)\n        'rename'     : new name of the column, if None will keep the original name, in case of duplicates suffix will be added (Default: None)\n\n        supported types: 'bool', 'int', 'float', 'str', 'date', 'time', 'datetime'\n\n        if any of the columns is rejected, entire row is rejected\n\n    tqdm: progressbar constructor\n    TaskManager: TaskManager constructor\n\n    returns: (Table, Table)\n        first table contains the rows that were successfully cast to desired types\n        second table contains rows that failed to cast + rejection reason\n    \"\"\"\n    return _column_select(self, cols, tqdm, TaskManager)\n
"},{"location":"reference/core/#tablite.core.Table.join","title":"tablite.core.Table.join(other, left_keys, right_keys, left_columns, right_columns, kind='inner', merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

short-cut for all join functions. kind: 'inner', 'left', 'outer', 'cross'

Source code in tablite/core.py
def join(self, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    short-cut for all join functions.\n    kind: 'inner', 'left', 'outer', 'cross'\n    \"\"\"\n    kinds = {\n        \"inner\": self.inner_join,\n        \"left\": self.left_join,\n        \"outer\": self.outer_join,\n        \"cross\": self.cross_join,\n    }\n    if kind not in kinds:\n        raise ValueError(f\"join type unknown: {kind}\")\n    f = kinds.get(kind, None)\n    return f(other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.left_join","title":"tablite.core.Table.left_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\nTablite: left_join = numbers.left_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n)\n
Source code in tablite/core.py
def left_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n    Tablite: left_join = numbers.left_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n    ```\n    \"\"\"\n    return joins.left_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.inner_join","title":"tablite.core.Table.inner_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\nTablite: inner_join = numbers.inner_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n
Source code in tablite/core.py
def inner_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    Tablite: inner_join = numbers.inner_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    ```\n    \"\"\"\n    return joins.inner_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.outer_join","title":"tablite.core.Table.outer_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\nTablite: outer_join = numbers.outer_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n
Source code in tablite/core.py
def outer_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    Tablite: outer_join = numbers.outer_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    ```\n    \"\"\"\n    return joins.outer_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.cross_join","title":"tablite.core.Table.cross_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

CROSS JOIN returns the Cartesian product of rows from tables in the join. In other words, it will produce rows which combine each row from the first table with each row from the second table

Source code in tablite/core.py
def cross_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    CROSS JOIN returns the Cartesian product of rows from tables in the join.\n    In other words, it will produce rows which combine each row from the first table\n    with each row from the second table\n    \"\"\"\n    return joins.cross_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.lookup","title":"tablite.core.Table.lookup(other, *criteria, all=True, tqdm=_tqdm)","text":"

function for looking up values in other according to criteria in ascending order. :param: other: Table sorted in ascending search order. :param: criteria: Each criteria must be a tuple with value comparisons in the form: (LEFT, OPERATOR, RIGHT) :param: all: boolean: True=ALL, False=Any

OPERATOR must be a callable that returns a boolean LEFT must be a value that the OPERATOR can compare. RIGHT must be a value that the OPERATOR can compare.

Examples:

('column A', \"==\", 'column B')  # comparison of two columns\n('Date', \"<\", DataTypes.date(24,12) )  # value from column 'Date' is before 24/12.\nf = lambda L,R: all( ord(L) < ord(R) )  # uses custom function.\n('text 1', f, 'text 2') value from column 'text 1' is compared with value from column 'text 2'\n
Source code in tablite/core.py
def lookup(self, other, *criteria, all=True, tqdm=_tqdm):\n    \"\"\"function for looking up values in `other` according to criteria in ascending order.\n    :param: other: Table sorted in ascending search order.\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n        (LEFT, OPERATOR, RIGHT)\n    :param: all: boolean: True=ALL, False=Any\n\n    OPERATOR must be a callable that returns a boolean\n    LEFT must be a value that the OPERATOR can compare.\n    RIGHT must be a value that the OPERATOR can compare.\n\n    Examples:\n    ```\n    ('column A', \"==\", 'column B')  # comparison of two columns\n    ('Date', \"<\", DataTypes.date(24,12) )  # value from column 'Date' is before 24/12.\n    f = lambda L,R: all( ord(L) < ord(R) )  # uses custom function.\n    ('text 1', f, 'text 2') value from column 'text 1' is compared with value from column 'text 2'\n    ```\n    \"\"\"\n    return lookup.lookup(self, other, *criteria, all=all, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.match","title":"tablite.core.Table.match(other, *criteria, keep_left=None, keep_right=None)","text":"

performs inner join where T matches other and removes rows that do not match.

:param: T: Table :param: other: Table :param: criteria: Each criteria must be a tuple with value comparisons in the form:

(LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\nExample:\n    ('column A', \"==\", 'column B')\n\nThis syntax follows the lookup syntax. See Lookup for details.\n

:param: keep_left: list of columns to keep. :param: keep_right: list of right columns to keep.

Source code in tablite/core.py
def match(self, other, *criteria, keep_left=None, keep_right=None):\n    \"\"\"\n    performs inner join where `T` matches `other` and removes rows that do not match.\n\n    :param: T: Table\n    :param: other: Table\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n\n        (LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\n        Example:\n            ('column A', \"==\", 'column B')\n\n        This syntax follows the lookup syntax. See Lookup for details.\n\n    :param: keep_left: list of columns to keep.\n    :param: keep_right: list of right columns to keep.\n    \"\"\"\n    return match.match(self, other, *criteria, keep_left=keep_left, keep_right=keep_right)\n
"},{"location":"reference/core/#tablite.core.Table.replace_missing_values","title":"tablite.core.Table.replace_missing_values(*args, **kwargs)","text":"Source code in tablite/core.py
def replace_missing_values(self, *args, **kwargs):\n    raise AttributeError(\"See imputation\")\n
"},{"location":"reference/core/#tablite.core.Table.imputation","title":"tablite.core.Table.imputation(targets, missing=None, method='carry forward', sources=None, tqdm=_tqdm)","text":"

In statistics, imputation is the process of replacing missing data with substituted values.

See more: https://en.wikipedia.org/wiki/Imputation_(statistics)

PARAMETER DESCRIPTION table

source table.

TYPE: Table

targets

column names to find and replace missing values

TYPE: str or list of strings

missing

values to be replaced.

TYPE: None or iterable DEFAULT: None

method

method to be used for replacement. Options:

'carry forward': takes the previous value, and carries forward into fields where values are missing. +: quick. Realistic on time series. -: Can produce strange outliers.

'mean': calculates the column mean (exclude missing) and copies the mean in as replacement. +: quick -: doesn't work on text. Causes data set to drift towards the mean.

'mode': calculates the column mode (exclude missing) and copies the mean in as replacement. +: quick -: most frequent value becomes over-represented in the sample

'nearest neighbour': calculates normalised distance between items in source columns selects nearest neighbour and copies value as replacement. +: works for any datatype. -: computationally intensive (e.g. slow)

TYPE: str DEFAULT: 'carry forward'

sources

NEAREST NEIGHBOUR ONLY column names to be used during imputation. if None or empty, all columns will be used.

TYPE: list of strings DEFAULT: None

RETURNS DESCRIPTION table

table with replaced values.

Source code in tablite/core.py
def imputation(self, targets, missing=None, method=\"carry forward\", sources=None, tqdm=_tqdm):\n    \"\"\"\n    In statistics, imputation is the process of replacing missing data with substituted values.\n\n    See more: https://en.wikipedia.org/wiki/Imputation_(statistics)\n\n    Args:\n        table (Table): source table.\n\n        targets (str or list of strings): column names to find and\n            replace missing values\n\n        missing (None or iterable): values to be replaced.\n\n        method (str): method to be used for replacement. Options:\n\n            'carry forward':\n                takes the previous value, and carries forward into fields\n                where values are missing.\n                +: quick. Realistic on time series.\n                -: Can produce strange outliers.\n\n            'mean':\n                calculates the column mean (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: doesn't work on text. Causes data set to drift towards the mean.\n\n            'mode':\n                calculates the column mode (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: most frequent value becomes over-represented in the sample\n\n            'nearest neighbour':\n                calculates normalised distance between items in source columns\n                selects nearest neighbour and copies value as replacement.\n                +: works for any datatype.\n                -: computationally intensive (e.g. slow)\n\n        sources (list of strings): NEAREST NEIGHBOUR ONLY\n            column names to be used during imputation.\n            if None or empty, all columns will be used.\n\n    Returns:\n        table: table with replaced values.\n    \"\"\"\n    return imputation.imputation(self, targets, missing, method, sources, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.transpose","title":"tablite.core.Table.transpose(tqdm=_tqdm)","text":"Source code in tablite/core.py
def transpose(self, tqdm=_tqdm):\n    return pivots.transpose(self, tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.pivot_transpose","title":"tablite.core.Table.pivot_transpose(columns, keep=None, column_name='transpose', value_name='value', tqdm=_tqdm)","text":"

Transpose a selection of columns to rows.

PARAMETER DESCRIPTION columns

column names to transpose

TYPE: list of column names

keep

column names to keep (repeat)

TYPE: list of column names DEFAULT: None

RETURNS DESCRIPTION Table

with columns transposed to rows

Example

transpose columns 1,2 and 3 and transpose the remaining columns, except sum.

Input:

| col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n|------|------|------|-----|-----|-----|-----|-----|------|\n| 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n| 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n| ...  |      |      |     |     |     |     |     |      |\n\nt.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\nOutput:\n\n|col1| col2| col3| transpose| value|\n|----|-----|-----|----------|------|\n|1234| 2345| 3456| sun      |   456|\n|1234| 2345| 3456| mon      |   567|\n|1244| 2445| 4456| mon      |     7|\n
Source code in tablite/core.py
def pivot_transpose(self, columns, keep=None, column_name=\"transpose\", value_name=\"value\", tqdm=_tqdm):\n    \"\"\"Transpose a selection of columns to rows.\n\n    Args:\n        columns (list of column names): column names to transpose\n        keep (list of column names): column names to keep (repeat)\n\n    Returns:\n        Table: with columns transposed to rows\n\n    Example:\n        transpose columns 1,2 and 3 and transpose the remaining columns, except `sum`.\n\n    Input:\n    ```\n    | col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n    |------|------|------|-----|-----|-----|-----|-----|------|\n    | 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n    | 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n    | ...  |      |      |     |     |     |     |     |      |\n\n    t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\n    Output:\n\n    |col1| col2| col3| transpose| value|\n    |----|-----|-----|----------|------|\n    |1234| 2345| 3456| sun      |   456|\n    |1234| 2345| 3456| mon      |   567|\n    |1244| 2445| 4456| mon      |     7|\n    ```\n    \"\"\"\n    return pivots.pivot_transpose(self, columns, keep, column_name, value_name, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.diff","title":"tablite.core.Table.diff(other, columns=None)","text":"

compares table self with table other

PARAMETER DESCRIPTION self

Table

TYPE: Table

other

Table

TYPE: Table

columns

list of column names to include in comparison. Defaults to None.

TYPE: List DEFAULT: None

RETURNS DESCRIPTION Table

diff of self and other with diff in columns 1st and 2nd.

Source code in tablite/core.py
def diff(self, other, columns=None):\n    \"\"\"compares table self with table other\n\n    Args:\n        self (Table): Table\n        other (Table): Table\n        columns (List, optional): list of column names to include in comparison. Defaults to None.\n\n    Returns:\n        Table: diff of self and other with diff in columns 1st and 2nd.\n    \"\"\"\n    return diff.diff(self, other, columns)\n
"},{"location":"reference/core/#tablite.core-functions","title":"Functions","text":""},{"location":"reference/core/#tablite.core-modules","title":"Modules","text":""},{"location":"reference/datasets/","title":"Datasets","text":""},{"location":"reference/datasets/#tablite.datasets","title":"tablite.datasets","text":""},{"location":"reference/datasets/#tablite.datasets-classes","title":"Classes","text":""},{"location":"reference/datasets/#tablite.datasets-functions","title":"Functions","text":""},{"location":"reference/datasets/#tablite.datasets.synthetic_order_data","title":"tablite.datasets.synthetic_order_data(rows=100000)","text":"

Creates a synthetic dataset for testing that looks like this: (depending on number of rows)

+=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n|    ~    |   #   |      1      |         2         |  3  | 4 |  5  | 6  | 7 |  8  |  9  |         10        |        11        |\n|   row   |  int  |     int     |      datetime     | int |int| int |str |str|mixed|mixed|       float       |      float       |\n+---------+-------+-------------+-------------------+-----+---+-----+----+---+-----+-----+-------------------+------------------+\n|0        |      1|1478158906743|2021-10-27 00:00:00|50764|  1|29990|C4-5|APP|21\u00b0  |None | 2.0434376837650046|1.3371665497020444|\n|1        |      2|2271295805011|2021-09-13 00:00:00|50141|  0|10212|C4-5|TAE|None |None |  1.010318612835485| 20.94821610676901|\n|2        |      3|1598726492913|2021-08-19 00:00:00|50527|  0|19416|C3-5|QPV|21\u00b0  |None |  1.463459515469516|  17.4133659842749|\n|3        |      4|1413615572689|2021-11-05 00:00:00|50181|  1|18637|C4-2|GCL|6\u00b0   |ABC  |  2.084002469706324| 0.489481411683505|\n|4        |      5| 245266998048|2021-09-25 00:00:00|50378|  0|29756|C5-4|LGY|6\u00b0   |XYZ  | 0.5141579343276079| 8.550780816571438|\n|5        |      6| 947994853644|2021-10-14 00:00:00|50511|  0| 7890|C2-4|BET|0\u00b0   |XYZ  | 1.1725893606177542| 7.447314130260951|\n|6        |      7|2230693047809|2021-10-07 00:00:00|50987|  1|26742|C1-3|CFP|0\u00b0   |XYZ  | 1.0921267279498004|11.009210185311993|\n|...      |...    |...          |...                |...  |...|...  |... |...|...  |...  |...                |...               |\n|7,999,993|7999994|2047223556745|2021-09-03 00:00:00|50883|  1|15687|C3-1|RFR|None |XYZ  | 1.3467185981566827|17.023443485654845|\n|7,999,994|7999995|1814140654790|2021-08-02 00:00:00|50152|  0|16556|C4-2|WTC|None |ABC  | 1.1517593924478968| 8.201818634721487|\n|7,999,995|7999996| 155308171103|2021-10-14 00:00:00|50008|  1|14590|C1-3|WYM|0\u00b0   |None | 2.1273836233717978|23.295943554889195|\n|7,999,996|7999997|1620451532911|2021-12-12 00:00:00|50173|  1|20744|C2-1|ZYO|6\u00b0   |ABC  |  2.482509134693724| 22.25375464857266|\n|7,999,997|7999998|1248987682094|2021-12-20 00:00:00|50052|  1|28298|C5-4|XAW|None |XYZ  |0.17923757926558143|23.728160892974252|\n|7,999,998|7999999|1382206732187|2021-11-13 00:00:00|50993|  1|24832|C5-2|UDL|None |ABC  |0.08425329763360942|12.707735293126758|\n|7,999,999|8000000| 600688069780|2021-09-28 00:00:00|50510|  0|15819|C3-4|IGY|None |ABC  |  1.066241687256579|13.862069804070295|\n+=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n
PARAMETER DESCRIPTION rows

number of rows wanted. Defaults to 100_000.

TYPE: int DEFAULT: 100000

RETURNS DESCRIPTION Table

Populated table.

TYPE: Table

Source code in tablite/datasets.py
def synthetic_order_data(rows=100_000):\n    \"\"\"Creates a synthetic dataset for testing that looks like this:\n    (depending on number of rows)\n\n    ```\n    +=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n    |    ~    |   #   |      1      |         2         |  3  | 4 |  5  | 6  | 7 |  8  |  9  |         10        |        11        |\n    |   row   |  int  |     int     |      datetime     | int |int| int |str |str|mixed|mixed|       float       |      float       |\n    +---------+-------+-------------+-------------------+-----+---+-----+----+---+-----+-----+-------------------+------------------+\n    |0        |      1|1478158906743|2021-10-27 00:00:00|50764|  1|29990|C4-5|APP|21\u00b0  |None | 2.0434376837650046|1.3371665497020444|\n    |1        |      2|2271295805011|2021-09-13 00:00:00|50141|  0|10212|C4-5|TAE|None |None |  1.010318612835485| 20.94821610676901|\n    |2        |      3|1598726492913|2021-08-19 00:00:00|50527|  0|19416|C3-5|QPV|21\u00b0  |None |  1.463459515469516|  17.4133659842749|\n    |3        |      4|1413615572689|2021-11-05 00:00:00|50181|  1|18637|C4-2|GCL|6\u00b0   |ABC  |  2.084002469706324| 0.489481411683505|\n    |4        |      5| 245266998048|2021-09-25 00:00:00|50378|  0|29756|C5-4|LGY|6\u00b0   |XYZ  | 0.5141579343276079| 8.550780816571438|\n    |5        |      6| 947994853644|2021-10-14 00:00:00|50511|  0| 7890|C2-4|BET|0\u00b0   |XYZ  | 1.1725893606177542| 7.447314130260951|\n    |6        |      7|2230693047809|2021-10-07 00:00:00|50987|  1|26742|C1-3|CFP|0\u00b0   |XYZ  | 1.0921267279498004|11.009210185311993|\n    |...      |...    |...          |...                |...  |...|...  |... |...|...  |...  |...                |...               |\n    |7,999,993|7999994|2047223556745|2021-09-03 00:00:00|50883|  1|15687|C3-1|RFR|None |XYZ  | 1.3467185981566827|17.023443485654845|\n    |7,999,994|7999995|1814140654790|2021-08-02 00:00:00|50152|  0|16556|C4-2|WTC|None |ABC  | 1.1517593924478968| 8.201818634721487|\n    |7,999,995|7999996| 155308171103|2021-10-14 00:00:00|50008|  1|14590|C1-3|WYM|0\u00b0   |None | 2.1273836233717978|23.295943554889195|\n    |7,999,996|7999997|1620451532911|2021-12-12 00:00:00|50173|  1|20744|C2-1|ZYO|6\u00b0   |ABC  |  2.482509134693724| 22.25375464857266|\n    |7,999,997|7999998|1248987682094|2021-12-20 00:00:00|50052|  1|28298|C5-4|XAW|None |XYZ  |0.17923757926558143|23.728160892974252|\n    |7,999,998|7999999|1382206732187|2021-11-13 00:00:00|50993|  1|24832|C5-2|UDL|None |ABC  |0.08425329763360942|12.707735293126758|\n    |7,999,999|8000000| 600688069780|2021-09-28 00:00:00|50510|  0|15819|C3-4|IGY|None |ABC  |  1.066241687256579|13.862069804070295|\n    +=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n    ```\n\n    Args:\n        rows (int, optional): number of rows wanted. Defaults to 100_000.\n\n    Returns:\n        Table (Table): Populated table.\n    \"\"\"  # noqa\n    rows = int(rows)\n\n    L1 = [\"None\", \"0\u00b0\", \"6\u00b0\", \"21\u00b0\"]\n    L2 = [\"ABC\", \"XYZ\", \"\"]\n\n    t = Table()\n    assert isinstance(t, Table)\n    for page_n in range(math.ceil(rows / Config.PAGE_SIZE)):  # n pages\n        start = (page_n * Config.PAGE_SIZE)\n        end = min(start + Config.PAGE_SIZE, rows)\n        ro = range(start, end)\n\n        t2 = Table()\n        t2[\"#\"] = [v+1 for v in ro]\n        # 1 - mock orderid\n        t2[\"1\"] = [random.randint(18_778_628_504, 2277_772_117_504) for i in ro]\n        # 2 - mock delivery date.\n        t2[\"2\"] = [datetime.fromordinal(random.randint(738000, 738150)).isoformat() for i in ro]\n        # 3 - mock store id.\n        t2[\"3\"] = [random.randint(50000, 51000) for _ in ro]\n        # 4 - random bit.\n        t2[\"4\"] = [random.randint(0, 1) for _ in ro]\n        # 5 - mock product id\n        t2[\"5\"] = [random.randint(3000, 30000) for _ in ro]\n        # 6 - random weird string\n        t2[\"6\"] = [f\"C{random.randint(1, 5)}-{random.randint(1, 5)}\" for _ in ro]\n        # 7 - # random category\n        t2[\"7\"] = [\"\".join(random.choice(ascii_uppercase) for _ in range(3)) for _ in ro]\n        # 8 -random temperature group.\n        t2[\"8\"] = [random.choice(L1) for _ in ro]\n        # 9 - random choice of category\n        t2[\"9\"] = [random.choice(L2) for _ in ro]\n        # 10 - volume?\n        t2[\"10\"] = [random.uniform(0.01, 2.5) for _ in ro]\n        # 11 - units?\n        t2[\"11\"] = [f\"{random.uniform(0.1, 25)}\" for _ in ro]\n\n        if len(t) == 0:\n            t = t2\n        else:\n            t += t2\n\n    return t\n
"},{"location":"reference/datatypes/","title":"Datatypes","text":""},{"location":"reference/datatypes/#tablite.datatypes","title":"tablite.datatypes","text":""},{"location":"reference/datatypes/#tablite.datatypes-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.matched_types","title":"tablite.datatypes.matched_types = {int: DataTypes._infer_int, str: DataTypes._infer_str, float: DataTypes._infer_float, bool: DataTypes._infer_bool, date: DataTypes._infer_date, datetime: DataTypes._infer_datetime, time: DataTypes._infer_time} module-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes-classes","title":"Classes","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes","title":"tablite.datatypes.DataTypes","text":"

Bases: object

DataTypes is the conversion library for all datatypes.

It supports any / all python datatypes.

"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.int","title":"tablite.datatypes.DataTypes.int = int class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.str","title":"tablite.datatypes.DataTypes.str = str class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.float","title":"tablite.datatypes.DataTypes.float = float class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.bool","title":"tablite.datatypes.DataTypes.bool = bool class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.date","title":"tablite.datatypes.DataTypes.date = date class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.datetime","title":"tablite.datatypes.DataTypes.datetime = datetime class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.time","title":"tablite.datatypes.DataTypes.time = time class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.timedelta","title":"tablite.datatypes.DataTypes.timedelta = timedelta class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.numeric_types","title":"tablite.datatypes.DataTypes.numeric_types = {int, float, date, time, datetime} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.epoch","title":"tablite.datatypes.DataTypes.epoch = datetime(2000, 1, 1, 0, 0, 0, 0, timezone.utc) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.epoch_no_tz","title":"tablite.datatypes.DataTypes.epoch_no_tz = datetime(2000, 1, 1, 0, 0, 0, 0) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.digits","title":"tablite.datatypes.DataTypes.digits = '1234567890' class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.decimals","title":"tablite.datatypes.DataTypes.decimals = set('1234567890-+eE.') class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.integers","title":"tablite.datatypes.DataTypes.integers = set('1234567890-+') class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.nones","title":"tablite.datatypes.DataTypes.nones = {'null', 'Null', 'NULL', '#N/A', '#n/a', '', 'None', None, np.nan} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.none_type","title":"tablite.datatypes.DataTypes.none_type = type(None) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.bytes_functions","title":"tablite.datatypes.DataTypes.bytes_functions = {type(None): b_none, bool: b_bool, int: b_int, float: b_float, str: b_str, bytes: b_bytes, datetime: b_datetime, date: b_date, time: b_time, timedelta: b_timedelta} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.type_code_functions","title":"tablite.datatypes.DataTypes.type_code_functions = {1: _none, 2: _bool, 3: _int, 4: _float, 5: _str, 6: _bytes, 7: _datetime, 8: _date, 9: _time, 10: _timedelta, 11: _unpickle} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.pytype_from_type_code","title":"tablite.datatypes.DataTypes.pytype_from_type_code = {1: type(None), 2: bool, 3: int, 4: float, 5: str, 6: bytes, 7: datetime, 8: date, 9: time, 10: timedelta, 11: 'pickled object'} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.date_formats","title":"tablite.datatypes.DataTypes.date_formats = {'NNNN-NN-NN': lambda : date(*int(i) for i in x.split('-')), 'NNNN-N-NN': lambda : date(*int(i) for i in x.split('-')), 'NNNN-NN-N': lambda : date(*int(i) for i in x.split('-')), 'NNNN-N-N': lambda : date(*int(i) for i in x.split('-')), 'NN-NN-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'N-NN-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'NN-N-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'N-N-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'NNNN.NN.NN': lambda : date(*int(i) for i in x.split('.')), 'NNNN.N.NN': lambda : date(*int(i) for i in x.split('.')), 'NNNN.NN.N': lambda : date(*int(i) for i in x.split('.')), 'NNNN.N.N': lambda : date(*int(i) for i in x.split('.')), 'NN.NN.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'N.NN.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'NN.N.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'N.N.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'NNNN/NN/NN': lambda : date(*int(i) for i in x.split('/')), 'NNNN/N/NN': lambda : date(*int(i) for i in x.split('/')), 'NNNN/NN/N': lambda : date(*int(i) for i in x.split('/')), 'NNNN/N/N': lambda : date(*int(i) for i in x.split('/')), 'NN/NN/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'N/NN/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'NN/N/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'N/N/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'NNNN NN NN': lambda : date(*int(i) for i in x.split(' ')), 'NNNN N NN': lambda : date(*int(i) for i in x.split(' ')), 'NNNN NN N': lambda : date(*int(i) for i in x.split(' ')), 'NNNN N N': lambda : date(*int(i) for i in x.split(' ')), 'NN NN NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'N N NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'NN N NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'N NN NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'NNNNNNNN': lambda : date(*(int(x[:4]), int(x[4:6]), int(x[6:])))} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.datetime_formats","title":"tablite.datatypes.DataTypes.datetime_formats = {'NNNN-NN-NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x), 'NNNN-NN-NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x), 'NNNN-NN-NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, T=' '), 'NNNN-NN-NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, T=' '), 'NNNN/NN/NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN/NN/NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN NN NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN NN NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN.NN.NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NNNN.NN.NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NN-NN-NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN/NN/NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN/NN/NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN NN NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN.NN.NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NNNNNNNNTNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, compact=3)} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.types","title":"tablite.datatypes.DataTypes.types = [datetime, date, time, int, bool, float, str] class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.type_code","title":"tablite.datatypes.DataTypes.type_code(value) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef type_code(cls, value):\n    if type(value) in cls._type_codes:\n        return cls._type_codes[type(value)]\n    elif hasattr(value, \"dtype\"):\n        dtype = pytype(value)\n        return cls._type_codes[dtype]\n    else:\n        return cls._type_codes[\"pickle\"]\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_none","title":"tablite.datatypes.DataTypes.b_none(v)","text":"Source code in tablite/datatypes.py
def b_none(v):\n    return b\"None\"\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_bool","title":"tablite.datatypes.DataTypes.b_bool(v)","text":"Source code in tablite/datatypes.py
def b_bool(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_int","title":"tablite.datatypes.DataTypes.b_int(v)","text":"Source code in tablite/datatypes.py
def b_int(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_float","title":"tablite.datatypes.DataTypes.b_float(v)","text":"Source code in tablite/datatypes.py
def b_float(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_str","title":"tablite.datatypes.DataTypes.b_str(v)","text":"Source code in tablite/datatypes.py
def b_str(v):\n    return v.encode(\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_bytes","title":"tablite.datatypes.DataTypes.b_bytes(v)","text":"Source code in tablite/datatypes.py
def b_bytes(v):\n    return v\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_datetime","title":"tablite.datatypes.DataTypes.b_datetime(v)","text":"Source code in tablite/datatypes.py
def b_datetime(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_date","title":"tablite.datatypes.DataTypes.b_date(v)","text":"Source code in tablite/datatypes.py
def b_date(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_time","title":"tablite.datatypes.DataTypes.b_time(v)","text":"Source code in tablite/datatypes.py
def b_time(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_timedelta","title":"tablite.datatypes.DataTypes.b_timedelta(v)","text":"Source code in tablite/datatypes.py
def b_timedelta(v):\n    return bytes(str(float(v.days + (v.seconds / (24 * 60 * 60)))), \"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_pickle","title":"tablite.datatypes.DataTypes.b_pickle(v)","text":"Source code in tablite/datatypes.py
def b_pickle(v):\n    return pickle.dumps(v, protocol=0)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.to_bytes","title":"tablite.datatypes.DataTypes.to_bytes(v) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef to_bytes(cls, v):\n    if type(v) in cls.bytes_functions:  # it's a python native type\n        f = cls.bytes_functions[type(v)]\n    elif hasattr(v, \"dtype\"):  # it's a numpy/c type.\n        dtype = pytype(v)\n        f = cls.bytes_functions[dtype]\n    else:\n        f = cls.b_pickle\n    return f(v)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.from_type_code","title":"tablite.datatypes.DataTypes.from_type_code(value, code) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef from_type_code(cls, value, code):\n    f = cls.type_code_functions[code]\n    return f(value)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.pattern_to_datetime","title":"tablite.datatypes.DataTypes.pattern_to_datetime(iso_string, ymd=None, T=None, compact=0, day_first=False) staticmethod","text":"Source code in tablite/datatypes.py
@staticmethod\ndef pattern_to_datetime(iso_string, ymd=None, T=None, compact=0, day_first=False):\n    assert isinstance(iso_string, str)\n    if compact:\n        s = iso_string\n        if compact == 1:  # has T\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (9, 11, \":\"),\n                (11, 13, \":\"),\n                (13, len(s), \"\"),\n            ]\n        elif compact == 2:  # has no T.\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (8, 10, \":\"),\n                (10, 12, \":\"),\n                (12, len(s), \"\"),\n            ]\n        elif compact == 3:  # has T and :\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (9, 11, \":\"),\n                (12, 14, \":\"),\n                (15, len(s), \"\"),\n            ]\n        else:\n            raise TypeError\n        iso_string = \"\".join([s[a:b] + c for a, b, c in slices if b <= len(s)])\n        iso_string = iso_string.rstrip(\":\")\n\n    if day_first:\n        s = iso_string\n        iso_string = \"\".join((s[6:10], \"-\", s[3:5], \"-\", s[0:2], s[10:]))\n\n    if \",\" in iso_string:\n        iso_string = iso_string.replace(\",\", \".\")\n\n    dot = iso_string[::-1].find(\".\")\n    if 0 < dot < 10:\n        ix = len(iso_string) - dot\n        microsecond = int(float(f\"0{iso_string[ix - 1:]}\") * 10**6)\n        # fmt:off\n        iso_string = iso_string[: len(iso_string) - dot] + str(microsecond).rjust(6, \"0\")\n        # fmt:on\n    if ymd:\n        iso_string = iso_string.replace(ymd, \"-\", 2)\n    if T:\n        iso_string = iso_string.replace(T, \"T\")\n    return datetime.fromisoformat(iso_string)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.round","title":"tablite.datatypes.DataTypes.round(value, multiple, up=None) classmethod","text":"

a nicer way to round numbers.

PARAMETER DESCRIPTION value

value to be rounded

TYPE: (float, integer, datetime)

multiple

value to be used as the based of rounding. 1) multiple = 1 is the same as rounding to whole integers. 2) multiple = 0.001 is the same as rounding to 3 digits precision. 3) mulitple = 3.1415 is rounding to nearest multiplier of 3.1415 4) value = datetime(2022,8,18,11,14,53,440) 5) multiple = timedelta(hours=0.5) 6) xround(value,multiple) is datetime(2022,8,18,11,0)

TYPE: (float, integer, timedelta)

up

None (default) or boolean rounds half, up or down. round(1.6, 1) rounds to 2. round(1.4, 1) rounds to 1. round(1.5, 1, up=True) rounds to 2. round(1.5, 1, up=False) rounds to 1.

TYPE: (None, bool) DEFAULT: None

RETURNS DESCRIPTION

float,integer,datetime: rounded value in same type as input.

Source code in tablite/datatypes.py
@classmethod\ndef round(cls, value, multiple, up=None):\n    \"\"\"a nicer way to round numbers.\n\n    Args:\n        value (float,integer,datetime): value to be rounded\n\n        multiple (float,integer,timedelta): value to be used as the based of rounding.\n            1) multiple = 1 is the same as rounding to whole integers.\n            2) multiple = 0.001 is the same as rounding to 3 digits precision.\n            3) mulitple = 3.1415 is rounding to nearest multiplier of 3.1415\n            4) value = datetime(2022,8,18,11,14,53,440)\n            5) multiple = timedelta(hours=0.5)\n            6) xround(value,multiple) is datetime(2022,8,18,11,0)\n\n        up (None, bool, optional):\n            None (default) or boolean rounds half, up or down.\n            round(1.6, 1) rounds to 2.\n            round(1.4, 1) rounds to 1.\n            round(1.5, 1, up=True) rounds to 2.\n            round(1.5, 1, up=False) rounds to 1.\n\n    Returns:\n        float,integer,datetime: rounded value in same type as input.\n    \"\"\"\n    epoch = 0\n    if isinstance(value, (datetime)) and isinstance(multiple, timedelta):\n        if value.tzinfo is None:\n            epoch = cls.epoch_no_tz\n        else:\n            epoch = cls.epoch\n\n    value2 = value - epoch\n    if value2 == 0:\n        return value2\n\n    low = (value2 // multiple) * multiple\n    high = low + multiple\n    if up is True:\n        return high + epoch\n    elif up is False:\n        return low + epoch\n    else:\n        if abs((high + epoch) - value) < abs(value - (low + epoch)):\n            return high + epoch\n        else:\n            return low + epoch\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.to_json","title":"tablite.datatypes.DataTypes.to_json(v) staticmethod","text":"

converts any python type to json.

PARAMETER DESCRIPTION v

value to convert to json

TYPE: any

RETURNS DESCRIPTION

json compatible value from v

Source code in tablite/datatypes.py
@staticmethod\ndef to_json(v):\n    \"\"\"converts any python type to json.\n\n    Args:\n        v (any): value to convert to json\n\n    Returns:\n        json compatible value from v\n    \"\"\"\n    if hasattr(v, \"dtype\"):\n        v = numpy_to_python(v)\n    if v is None:\n        return v\n    elif v is False:\n        # using isinstance(v, bool): won't work as False also is int of zero.\n        return str(v)\n    elif v is True:\n        return str(v)\n    elif isinstance(v, int):\n        return v\n    elif isinstance(v, str):\n        return v\n    elif isinstance(v, float):\n        return v\n    elif isinstance(v, datetime):\n        return v.isoformat()\n    elif isinstance(v, time):\n        return v.isoformat()\n    elif isinstance(v, date):\n        return v.isoformat()\n    elif isinstance(v, timedelta):\n        return f\"P{v.days}DT{v.seconds + (v.microseconds / 1e6)}S\"\n    else:\n        raise TypeError(f\"The datatype {type(v)} is not supported.\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.from_json","title":"tablite.datatypes.DataTypes.from_json(v, dtype) staticmethod","text":"

converts json to python datatype

PARAMETER DESCRIPTION v

value

TYPE: any

dtype

any python type

TYPE: python type

RETURNS DESCRIPTION

python type of value v

Source code in tablite/datatypes.py
@staticmethod\ndef from_json(v, dtype):\n    \"\"\"converts json to python datatype\n\n    Args:\n        v (any): value\n        dtype (python type): any python type\n\n    Returns:\n        python type of value v\n    \"\"\"\n    if v in DataTypes.nones:\n        if dtype is str and v == \"\":\n            return \"\"\n        else:\n            return None\n    if dtype is int:\n        return int(v)\n    elif dtype is str:\n        return str(v)\n    elif dtype is float:\n        return float(v)\n    elif dtype is bool:\n        if v == \"False\":\n            return False\n        elif v == \"True\":\n            return True\n        else:\n            raise ValueError(v)\n    elif dtype is date:\n        return date.fromisoformat(v)\n    elif dtype is datetime:\n        return datetime.fromisoformat(v)\n    elif dtype is time:\n        return time.fromisoformat(v)\n    elif dtype is timedelta:\n        L = v.split(\"DT\")\n        days = int(L[0].lstrip(\"P\"))\n        seconds = float(L[1].rstrip(\"S\"))\n        return timedelta(days, seconds)\n    else:\n        raise TypeError(f\"The datatype {str(dtype)} is not supported.\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.guess_types","title":"tablite.datatypes.DataTypes.guess_types(*values) staticmethod","text":"

Attempts to guess the datatype for *values returns dict with matching datatypes and probabilities

RETURNS DESCRIPTION dict

{key: type, value: probability}

Source code in tablite/datatypes.py
@staticmethod\ndef guess_types(*values):\n    \"\"\"Attempts to guess the datatype for *values\n    returns dict with matching datatypes and probabilities\n\n    Returns:\n        dict: {key: type, value: probability}\n    \"\"\"\n    d = defaultdict(int)\n    probability = Rank(DataTypes.types[:])\n\n    for value in values:\n        if hasattr(value, \"dtype\"):\n            value = numpy_to_python(value)\n\n        for dtype in probability:\n            try:\n                _ = DataTypes.infer(value, dtype)\n                d[dtype] += 1\n                probability.match(dtype)\n                break\n            except (ValueError, TypeError):\n                pass\n    if not d:\n        d[str] = len(values)\n    return {k: round(v / len(values), 3) for k, v in d.items()}\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.guess","title":"tablite.datatypes.DataTypes.guess(*values) staticmethod","text":"

Makes a best guess the datatype for *values returns list of native python values

RETURNS DESCRIPTION list

list of native python values

Source code in tablite/datatypes.py
@staticmethod\ndef guess(*values):\n    \"\"\"Makes a best guess the datatype for *values\n    returns list of native python values\n\n    Returns:\n        list: list of native python values\n    \"\"\"\n    probability = Rank(*DataTypes.types[:])\n    matches = [None for _ in values[0]]\n\n    for ix, value in enumerate(values[0]):\n        if hasattr(value, \"dtype\"):\n            value = numpy_to_python(value)\n        for dtype in probability:\n            try:\n                matches[ix] = DataTypes.infer(value, dtype)\n                probability.match(dtype)\n                break\n            except (ValueError, TypeError):\n                pass\n    return matches\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.infer","title":"tablite.datatypes.DataTypes.infer(v, dtype) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef infer(cls, v, dtype):\n    if isinstance(v, str) and dtype == str:\n        # we got a string, we're trying to infer it to string, we shouldn't check for None-ness\n        return v\n\n    if v in DataTypes.nones:\n        return None\n\n    if dtype not in matched_types:\n        raise TypeError(f\"The datatype {str(dtype)} is not supported.\")\n\n    return matched_types[dtype](v)\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank","title":"tablite.datatypes.Rank(*items)","text":"

Bases: object

Source code in tablite/datatypes.py
def __init__(self, *items):\n    self.items = {i: ix for i, ix in zip(items, range(len(items)))}\n    self.ranks = [0 for _ in items]\n    self.items_list = [i for i in items]\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.items","title":"tablite.datatypes.Rank.items = {i: ixfor (i, ix) in zip(items, range(len(items)))} instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.ranks","title":"tablite.datatypes.Rank.ranks = [0 for _ in items] instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.items_list","title":"tablite.datatypes.Rank.items_list = [i for i in items] instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.match","title":"tablite.datatypes.Rank.match(k)","text":"Source code in tablite/datatypes.py
def match(self, k):  # k+=1\n    ix = self.items[k]\n    r = self.ranks\n    r[ix] += 1\n\n    if ix > 0:\n        p = self.items_list\n        while (\n            r[ix] > r[ix - 1] and ix > 0\n        ):  # use a simple bubble sort to maintain rank\n            r[ix], r[ix - 1] = r[ix - 1], r[ix]\n            p[ix], p[ix - 1] = p[ix - 1], p[ix]\n            old = p[ix]\n            self.items[old] = ix\n            self.items[k] = ix - 1\n            ix -= 1\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank.__iter__","title":"tablite.datatypes.Rank.__iter__()","text":"Source code in tablite/datatypes.py
def __iter__(self):\n    return iter(self.items_list)\n
"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray","title":"tablite.datatypes.MetaArray","text":"

Bases: ndarray

Array with metadata.

"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.MetaArray.__new__","title":"tablite.datatypes.MetaArray.__new__(array, dtype=None, order=None, **kwargs)","text":"Source code in tablite/datatypes.py
def __new__(cls, array, dtype=None, order=None, **kwargs):\n    obj = np.asarray(array, dtype=dtype, order=order).view(cls)\n    obj.metadata = kwargs\n    return obj\n
"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray.__array_finalize__","title":"tablite.datatypes.MetaArray.__array_finalize__(obj)","text":"Source code in tablite/datatypes.py
def __array_finalize__(self, obj):\n    if obj is None:\n        return\n    self.metadata = getattr(obj, \"metadata\", None)\n
"},{"location":"reference/datatypes/#tablite.datatypes-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.numpy_to_python","title":"tablite.datatypes.numpy_to_python(obj: Any) -> Any","text":"

Converts numpy types to python types.

See https://numpy.org/doc/stable/reference/arrays.scalars.html

PARAMETER DESCRIPTION obj

A numpy object

TYPE: Any

RETURNS DESCRIPTION Any

python object: A python object

Source code in tablite/datatypes.py
def numpy_to_python(obj: Any) -> Any:\n    \"\"\"Converts numpy types to python types.\n\n    See https://numpy.org/doc/stable/reference/arrays.scalars.html\n\n    Args:\n        obj (Any): A numpy object\n\n    Returns:\n        python object: A python object\n    \"\"\"\n    if isinstance(obj, np.generic):\n        return obj.item()\n    return obj\n
"},{"location":"reference/datatypes/#tablite.datatypes.pytype","title":"tablite.datatypes.pytype(obj)","text":"

Returns the python type of any object

PARAMETER DESCRIPTION obj

any numpy or python object

TYPE: Any

RETURNS DESCRIPTION type

type of obj

Source code in tablite/datatypes.py
def pytype(obj):\n    \"\"\"Returns the python type of any object\n\n    Args:\n        obj (Any): any numpy or python object\n\n    Returns:\n        type: type of obj\n    \"\"\"\n    if isinstance(obj, np.generic):\n        return type(obj.item())\n    return type(obj)\n
"},{"location":"reference/datatypes/#tablite.datatypes.pytype_from_iterable","title":"tablite.datatypes.pytype_from_iterable(iterable: {tuple, list}) -> {np.dtype, dict}","text":"

helper to make correct np array from python types.

PARAMETER DESCRIPTION iterable

values to be converted to numpy array.

TYPE: (tuple, list)

RAISES DESCRIPTION NotImplementedError

if datatype is not supported.

RETURNS DESCRIPTION {dtype, dict}

np.dtype: python type of the iterable.

Source code in tablite/datatypes.py
def pytype_from_iterable(iterable: {tuple, list}) -> {np.dtype, dict}:\n    \"\"\"helper to make correct np array from python types.\n\n    Args:\n        iterable (tuple,list): values to be converted to numpy array.\n\n    Raises:\n        NotImplementedError: if datatype is not supported.\n\n    Returns:\n        np.dtype: python type of the iterable.\n    \"\"\"\n    py_types = {}\n    if isinstance(iterable, (tuple, list)):\n        type_counter = Counter((pytype(v) for v in iterable))\n\n        for k, v in type_counter.items():\n            py_types[k] = v\n\n        if len(py_types) == 0:\n            np_dtype, py_dtype = object, bool\n        elif len(py_types) == 1:\n            py_dtype = list(py_types.keys())[0]\n            if py_dtype == datetime:\n                np_dtype = np.datetime64\n            elif py_dtype == date:\n                np_dtype = np.datetime64\n            elif py_dtype == timedelta:\n                np_dtype = np.timedelta64\n            else:\n                np_dtype = None\n        else:\n            np_dtype = object\n    elif isinstance(iterable, np.ndarray):\n        if iterable.dtype == object:\n            np_dtype = object\n            py_types = dict(Counter((pytype(v) for v in iterable)))\n        else:\n            np_dtype = iterable.dtype\n            if len(iterable) > 0:\n                py_types = {pytype(iterable[0]): len(iterable)}\n            else:\n                py_types = {pytype(np_dtype.type()): len(iterable)}\n    else:\n        raise NotImplementedError(f\"No handler for {type(iterable)}\")\n\n    return np_dtype, py_types\n
"},{"location":"reference/datatypes/#tablite.datatypes.list_to_np_array","title":"tablite.datatypes.list_to_np_array(iterable)","text":"

helper to make correct np array from python types. Example of problem where numpy turns mixed types into strings.

np.array([4, '5']) np.ndarray(['4', '5'])

RETURNS DESCRIPTION

np.array

datatypes

Source code in tablite/datatypes.py
def list_to_np_array(iterable):\n    \"\"\"helper to make correct np array from python types.\n    Example of problem where numpy turns mixed types into strings.\n    >>> np.array([4, '5'])\n    np.ndarray(['4', '5'])\n\n    returns:\n        np.array\n        datatypes\n    \"\"\"\n    np_dtype, py_dtype = pytype_from_iterable(iterable)\n\n    value = MetaArray(iterable, dtype=np_dtype, py_dtype=py_dtype)\n    return value\n
"},{"location":"reference/datatypes/#tablite.datatypes.np_type_unify","title":"tablite.datatypes.np_type_unify(arrays)","text":"

unifies numpy types.

PARAMETER DESCRIPTION arrays

List of numpy arrays

TYPE: list

RETURNS DESCRIPTION

np.ndarray: numpy array of a single type.

Source code in tablite/datatypes.py
def np_type_unify(arrays):\n    \"\"\"unifies numpy types.\n\n    Args:\n        arrays (list): List of numpy arrays\n\n    Returns:\n        np.ndarray: numpy array of a single type.\n    \"\"\"\n    dtypes = {arr.dtype: len(arr) for arr in arrays}\n    if len(dtypes) == 1:\n        dtype, _ = dtypes.popitem()\n    else:\n        for ix, arr in enumerate(arrays):\n            arrays[ix] = np.array(arr, dtype=object)\n        dtype = object\n    return np.concatenate(arrays, dtype=dtype)\n
"},{"location":"reference/datatypes/#tablite.datatypes.multitype_set","title":"tablite.datatypes.multitype_set(arr)","text":"

prevents loss of True, False when calling sets.

python looses values when called returning a set. Example:

{1, True, 0, False}

PARAMETER DESCRIPTION arr

iterable of mixed types.

TYPE: Iterable

RETURNS DESCRIPTION

np.array: with unique values.

Source code in tablite/datatypes.py
def multitype_set(arr):\n    \"\"\"prevents loss of True, False when calling sets.\n\n    python looses values when called returning a set. Example:\n    >>> {1, True, 0, False}\n    {0,1}\n\n    Args:\n        arr (Iterable): iterable of mixed types.\n\n    Returns:\n        np.array: with unique values.\n    \"\"\"\n    L = [(type(v), v) for v in arr]\n    L = list(set(L))\n    L = [v for _, v in L]\n    return np.array(L, dtype=object)\n
"},{"location":"reference/diff/","title":"Diff","text":""},{"location":"reference/diff/#tablite.diff","title":"tablite.diff","text":""},{"location":"reference/diff/#tablite.diff-classes","title":"Classes","text":""},{"location":"reference/diff/#tablite.diff-functions","title":"Functions","text":""},{"location":"reference/diff/#tablite.diff.diff","title":"tablite.diff.diff(T, other, columns=None)","text":"

compares table self with table other

PARAMETER DESCRIPTION self

Table

TYPE: Table

other

Table

TYPE: Table

columns

list of column names to include in comparison. Defaults to None.

TYPE: List DEFAULT: None

RETURNS DESCRIPTION Table

diff of self and other with diff in columns 1st and 2nd.

Source code in tablite/diff.py
def diff(T, other, columns=None):\n    \"\"\"compares table self with table other\n\n    Args:\n        self (Table): Table\n        other (Table): Table\n        columns (List, optional): list of column names to include in comparison. Defaults to None.\n\n    Returns:\n        Table: diff of self and other with diff in columns 1st and 2nd.\n    \"\"\"\n    sub_cls_check(T, Table)\n    sub_cls_check(other, Table)\n    if columns is None:\n        columns = [name for name in T.columns if name in other.columns]\n    elif isinstance(columns, list) and all(isinstance(i, str) for i in columns):\n        for name in columns:\n            if name not in T.columns:\n                raise ValueError(f\"column '{name}' not found\")\n            if name not in other.columns:\n                raise ValueError(f\"column '{name}' not found\")\n    else:\n        raise TypeError(\"Expected list of column names\")\n\n    t1 = T[columns]\n    if issubclass(type(t1), Table):\n        t1 = [tuple(r) for r in T.rows]\n    else:\n        t1 = list(T)\n    t2 = other[columns]\n    if issubclass(type(t2), Table):\n        t2 = [tuple(r) for r in other.rows]\n    else:\n        t2 = list(other)\n\n    sm = difflib.SequenceMatcher(None, t1, t2)\n    new = type(T)()\n    first = unique_name(\"1st\", columns)\n    second = unique_name(\"2nd\", columns)\n    new.add_columns(*columns + [first, second])\n\n    news = {n: [] for n in new.columns}  # Cache for Work in progress.\n\n    for opc, t1a, t1b, t2a, t2b in sm.get_opcodes():\n        if opc == \"insert\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"-\"] * (t2b - t2a)\n            news[second] += [\"+\"] * (t2b - t2a)\n\n        elif opc == \"delete\":\n            for name, col in zip(columns, zip(*t1[t1a:t1b])):\n                news[name].extend(col)\n            news[first] += [\"+\"] * (t1b - t1a)\n            news[second] += [\"-\"] * (t1b - t1a)\n\n        elif opc == \"equal\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"=\"] * (t2b - t2a)\n            news[second] += [\"=\"] * (t2b - t2a)\n\n        elif opc == \"replace\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"r\"] * (t2b - t2a)\n            news[second] += [\"r\"] * (t2b - t2a)\n\n        else:\n            pass\n\n        # Clear cache to free up memory.\n        if len(news[first]) > Config.PAGE_SIZE == 0:\n            for name, L in news.items():\n                new[name].extend(np.array(L))\n                L.clear()\n\n    for name, L in news.items():\n        new[name].extend(np.array(L))\n        L.clear()\n    return new\n
"},{"location":"reference/export_utils/","title":"Export utils","text":""},{"location":"reference/export_utils/#tablite.export_utils","title":"tablite.export_utils","text":""},{"location":"reference/export_utils/#tablite.export_utils-classes","title":"Classes","text":""},{"location":"reference/export_utils/#tablite.export_utils-functions","title":"Functions","text":""},{"location":"reference/export_utils/#tablite.export_utils.to_sql","title":"tablite.export_utils.to_sql(table, name)","text":"

generates ANSI-92 compliant SQL.

PARAMETER DESCRIPTION name

name of SQL table.

TYPE: str

Source code in tablite/export_utils.py
def to_sql(table, name):\n    \"\"\"\n    generates ANSI-92 compliant SQL.\n\n    args:\n        name (str): name of SQL table.\n    \"\"\"\n    sub_cls_check(table, Table)\n    type_check(name, str)\n\n    prefix = name\n    name = \"T1\"\n    create_table = \"\"\"CREATE TABLE {} ({})\"\"\"\n    columns = []\n    for name, col in table.columns.items():\n        dtype = col.types()\n        if len(dtype) == 1:\n            dtype, _ = dtype.popitem()\n            if dtype is int:\n                dtype = \"INTEGER\"\n            elif dtype is float:\n                dtype = \"REAL\"\n            else:\n                dtype = \"TEXT\"\n        else:\n            dtype = \"TEXT\"\n        definition = f\"{name} {dtype}\"\n        columns.append(definition)\n\n    create_table = create_table.format(prefix, \", \".join(columns))\n\n    # return create_table\n    row_inserts = []\n    for row in table.rows:\n        row_inserts.append(str(tuple([i if i is not None else \"NULL\" for i in row])))\n    row_inserts = f\"INSERT INTO {prefix} VALUES \" + \",\".join(row_inserts)\n    return \"begin; {}; {}; commit;\".format(create_table, row_inserts)\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_pandas","title":"tablite.export_utils.to_pandas(table)","text":"

returns pandas.DataFrame

Source code in tablite/export_utils.py
def to_pandas(table):\n    \"\"\"\n    returns pandas.DataFrame\n    \"\"\"\n    sub_cls_check(table, Table)\n    try:\n        return pd.DataFrame(table.to_dict())  # noqa\n    except ImportError:\n        import pandas as pd  # noqa\n    return pd.DataFrame(table.to_dict())  # noqa\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_hdf5","title":"tablite.export_utils.to_hdf5(table, path)","text":"

creates a copy of the table as hdf5

Note that some loss of type information is to be expected in columns of mixed type:

t.show(dtype=True) +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|str |mixed| bool| datetime | date | time | timedelta |str| int |float|int| +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1| |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1|1000|1 | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ t.to_hdf5(filename) t2 = Table.from_hdf5(filename) t2.show(dtype=True) +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|mixed|mixed| bool| datetime | datetime | time | str |str| int |float|int| +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1| 1000| 1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+

Source code in tablite/export_utils.py
def to_hdf5(table, path):\n    # fmt: off\n    \"\"\"\n    creates a copy of the table as hdf5\n\n    Note that some loss of type information is to be expected in columns of mixed type:\n    >>> t.show(dtype=True)\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  | D  |  E  |  F  |         G         |    H     |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|str |mixed| bool|      datetime     |   date   |  time  |   timedelta   |str|           int           |float|int|\n    +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|    |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1|1000|1    | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8  | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    >>> t.to_hdf5(filename)\n    >>> t2 = Table.from_hdf5(filename)\n    >>> t2.show(dtype=True)\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  |  D  |  E  |  F  |         G         |         H         |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|mixed|mixed| bool|      datetime     |      datetime     |  time  |      str      |str|           int           |float|int|\n    +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1| 1000|    1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8  | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    \"\"\"\n    # fmt: in\n    import h5py\n\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    total = f\"{len(table.columns) * len(table):,}\"  # noqa\n    print(f\"writing {total} records to {path}\", end=\"\")\n\n    with h5py.File(path, \"w\") as f:\n        n = 0\n        for name, col in table.items():\n            try:\n                f.create_dataset(name, data=col[:])  # stored in hdf5 as '/name'\n            except TypeError:\n                f.create_dataset(name, data=[str(i) for i in col[:]])  # stored in hdf5 as '/name'\n            n += 1\n    print(\"... done\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.excel_writer","title":"tablite.export_utils.excel_writer(table, path)","text":"

writer for excel files.

This can create xlsx files beyond Excels. If you're using pyexcel to read the data, you'll see the data is there. If you're using Excel, Excel will stop loading after 1,048,576 rows.

See pyexcel for more details: http://docs.pyexcel.org/

Source code in tablite/export_utils.py
def excel_writer(table, path):\n    \"\"\"\n    writer for excel files.\n\n    This can create xlsx files beyond Excels.\n    If you're using pyexcel to read the data, you'll see the data is there.\n    If you're using Excel, Excel will stop loading after 1,048,576 rows.\n\n    See pyexcel for more details:\n    http://docs.pyexcel.org/\n    \"\"\"\n    import pyexcel\n\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    def gen(table):  # local helper\n        yield table.columns\n        for row in table.rows:\n            yield row\n\n    data = list(gen(table))\n    if path.suffix in [\".xls\", \".ods\"]:\n        data = [\n            [str(v) if (isinstance(v, (int, float)) and abs(v) > 2**32 - 1) else DataTypes.to_json(v) for v in row]\n            for row in data\n        ]\n\n    pyexcel.save_as(array=data, dest_file_name=str(path))\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_json","title":"tablite.export_utils.to_json(table, *args, **kwargs)","text":"Source code in tablite/export_utils.py
def to_json(table, *args, **kwargs):\n    import json\n\n    sub_cls_check(table, Table)\n    return json.dumps(table.as_json_serializable())\n
"},{"location":"reference/export_utils/#tablite.export_utils.path_suffix_check","title":"tablite.export_utils.path_suffix_check(path, kind)","text":"Source code in tablite/export_utils.py
def path_suffix_check(path, kind):\n    if not path.suffix == kind:\n        raise ValueError(f\"Suffix mismatch: Expected {kind}, got {path.suffix} in {path.name}\")\n    if not path.parent.exists():\n        raise FileNotFoundError(f\"directory {path.parent} not found.\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.text_writer","title":"tablite.export_utils.text_writer(table, path, tqdm=_tqdm)","text":"

exports table to csv, tsv or txt dependening on path suffix. follows the JSON norm. text escape is ON for all strings.

"},{"location":"reference/export_utils/#tablite.export_utils.text_writer--note","title":"Note:","text":"

If the delimiter is present in a string when the string is exported, text-escape is required, as the format otherwise is corrupted. When the file is being written, it is unknown whether any string in a column contrains the delimiter. As text escaping the few strings that may contain the delimiter would lead to an assymmetric format, the safer guess is to text escape all strings.

Source code in tablite/export_utils.py
def text_writer(table, path, tqdm=_tqdm):\n    \"\"\"exports table to csv, tsv or txt dependening on path suffix.\n    follows the JSON norm. text escape is ON for all strings.\n\n    Note:\n    ----------------------\n    If the delimiter is present in a string when the string is exported,\n    text-escape is required, as the format otherwise is corrupted.\n    When the file is being written, it is unknown whether any string in\n    a column contrains the delimiter. As text escaping the few strings\n    that may contain the delimiter would lead to an assymmetric format,\n    the safer guess is to text escape all strings.\n    \"\"\"\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    def txt(value):  # helper for text writer\n        if value is None:\n            return \"\"  # A column with 1,None,2 must be \"1,,2\".\n        elif isinstance(value, str):\n            # if not (value.startswith('\"') and value.endswith('\"')):\n            #     return f'\"{value}\"'  # this must be escape: \"the quick fox, jumped over the comma\"\n            # else:\n            return value  # this would for example be an empty string: \"\"\n        else:\n            return str(DataTypes.to_json(value))  # this handles datetimes, timedelta, etc.\n\n    delimiters = {\".csv\": \",\", \".tsv\": \"\\t\", \".txt\": \"|\"}\n    delimiter = delimiters.get(path.suffix)\n\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(delimiter.join(c for c in table.columns) + \"\\n\")\n        for row in tqdm(table.rows, total=len(table), disable=Config.TQDM_DISABLE):\n            fo.write(delimiter.join(txt(c) for c in row) + \"\\n\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.sql_writer","title":"tablite.export_utils.sql_writer(table, path)","text":"Source code in tablite/export_utils.py
def sql_writer(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(to_sql(table))\n
"},{"location":"reference/export_utils/#tablite.export_utils.json_writer","title":"tablite.export_utils.json_writer(table, path)","text":"Source code in tablite/export_utils.py
def json_writer(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\") as fo:\n        fo.write(to_json(table))\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_html","title":"tablite.export_utils.to_html(table, path)","text":"Source code in tablite/export_utils.py
def to_html(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(table._repr_html_(slice(0, len(table))))\n
"},{"location":"reference/file_reader_utils/","title":"File reader utils","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils","title":"tablite.file_reader_utils","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-attributes","title":"Attributes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.ENCODING_GUESS_BYTES","title":"tablite.file_reader_utils.ENCODING_GUESS_BYTES = 10000 module-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-classes","title":"Classes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape","title":"tablite.file_reader_utils.TextEscape(openings='({[', closures=']})', text_qualifier='\"', delimiter=',', strip_leading_and_tailing_whitespace=False)","text":"

Bases: object

enables parsing of CSV with respecting brackets and text marks.

Example: text_escape = TextEscape() # set up the instance. for line in somefile.readlines(): list_of_words = text_escape(line) # use the instance. ...

As an example, the Danes and Germans use \" for inches and ' for feet, so we will see data that contains nail (75 x 4 mm, 3\" x 3/12\"), so for this case ( and ) are valid escapes, but \" and ' aren't.

Source code in tablite/file_reader_utils.py
def __init__(\n    self,\n    openings=\"({[\",\n    closures=\"]})\",\n    text_qualifier='\"',\n    delimiter=\",\",\n    strip_leading_and_tailing_whitespace=False,\n):\n    \"\"\"\n    As an example, the Danes and Germans use \" for inches and ' for feet,\n    so we will see data that contains nail (75 x 4 mm, 3\" x 3/12\"), so\n    for this case ( and ) are valid escapes, but \" and ' aren't.\n\n    \"\"\"\n    if openings is None:\n        openings = [None]\n    elif isinstance(openings, str):\n        self.openings = {c for c in openings}\n    else:\n        raise TypeError(f\"expected str, got {type(openings)}\")\n\n    if closures is None:\n        closures = [None]\n    elif isinstance(closures, str):\n        self.closures = {c for c in closures}\n    else:\n        raise TypeError(f\"expected str, got {type(closures)}\")\n\n    if not isinstance(delimiter, str):\n        raise TypeError(f\"expected str, got {type(delimiter)}\")\n    self.delimiter = delimiter\n    self._delimiter_length = len(delimiter)\n    self.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace\n\n    if text_qualifier is None:\n        pass\n    elif text_qualifier in openings + closures:\n        raise ValueError(\"It's a bad idea to have qoute character appears in openings or closures.\")\n    else:\n        self.qoute = text_qualifier\n\n    if not text_qualifier:\n        if not self.strip_leading_and_tailing_whitespace:\n            self.c = self._call_1\n        else:\n            self.c = self._call_2\n    else:\n        self.c = self._call_3\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape-attributes","title":"Attributes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.openings","title":"tablite.file_reader_utils.TextEscape.openings = {c for c in openings} instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.closures","title":"tablite.file_reader_utils.TextEscape.closures = {c for c in closures} instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.delimiter","title":"tablite.file_reader_utils.TextEscape.delimiter = delimiter instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.strip_leading_and_tailing_whitespace","title":"tablite.file_reader_utils.TextEscape.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.qoute","title":"tablite.file_reader_utils.TextEscape.qoute = text_qualifier instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.c","title":"tablite.file_reader_utils.TextEscape.c = self._call_1 instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape-functions","title":"Functions","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.__call__","title":"tablite.file_reader_utils.TextEscape.__call__(s)","text":"Source code in tablite/file_reader_utils.py
def __call__(self, s):\n    return self.c(s)\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-functions","title":"Functions","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.split_by_sequence","title":"tablite.file_reader_utils.split_by_sequence(text, sequence)","text":"

helper to split text according to a split sequence.

Source code in tablite/file_reader_utils.py
def split_by_sequence(text, sequence):\n    \"\"\"helper to split text according to a split sequence.\"\"\"\n    chunks = tuple()\n    for element in sequence:\n        idx = text.find(element)\n        if idx < 0:\n            raise ValueError(f\"'{element}' not in row\")\n        chunk, text = text[:idx], text[len(element) + idx :]\n        chunks += (chunk,)\n    chunks += (text,)  # the remaining text.\n    return chunks\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.detect_seperator","title":"tablite.file_reader_utils.detect_seperator(text)","text":"

:param path: pathlib.Path objects :param encoding: file encoding. :return: 1 character.

Source code in tablite/file_reader_utils.py
def detect_seperator(text):\n    \"\"\"\n    :param path: pathlib.Path objects\n    :param encoding: file encoding.\n    :return: 1 character.\n    \"\"\"\n    # After reviewing the logic in the CSV sniffer, I concluded that all it\n    # really does is to look for a non-text character. As the separator is\n    # determined by the first line, which almost always is a line of headers,\n    # the text characters will be utf-8,16 or ascii letters plus white space.\n    # This leaves the characters ,;:| and \\t as potential separators, with one\n    # exception: files that use whitespace as separator. My logic is therefore\n    # to (1) find the set of characters that intersect with ',;:|\\t' which in\n    # practice is a single character, unless (2) it is empty whereby it must\n    # be whitespace.\n    if len(text) == 0:\n        return None\n    seps = {\",\", \"\\t\", \";\", \":\", \"|\"}.intersection(text)\n    if not seps:\n        if \" \" in text:\n            return \" \"\n        if \"\\n\" in text:\n            return \"\\n\"\n        else:\n            raise ValueError(\"separator not detected\")\n    if len(seps) == 1:\n        return seps.pop()\n    else:\n        frq = [(text.count(i), i) for i in seps]\n        frq.sort(reverse=True)  # most frequent first.\n        return frq[0][-1]\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_headers","title":"tablite.file_reader_utils.get_headers(path, delimiter=None, header_row_index=0, text_qualifier=None, linecount=10)","text":"

file format definition csv comma separated values tsv tab separated values csvz a zip file that contains one or many csv files tsvz a zip file that contains one or many tsv files xls a spreadsheet file format created by MS-Excel 97-2003 xlsx MS-Excel Extensions to the Office Open XML SpreadsheetML File Format. xlsm an MS-Excel Macro-Enabled Workbook file ods open document spreadsheet fods flat open document spreadsheet json java script object notation html html table of the data structure simple simple presentation rst rStructured Text presentation of the data mediawiki media wiki table

Source code in tablite/file_reader_utils.py
def get_headers(path, delimiter=None, header_row_index=0, text_qualifier=None, linecount=10):\n    \"\"\"\n    file format\tdefinition\n    csv\t    comma separated values\n    tsv\t    tab separated values\n    csvz\ta zip file that contains one or many csv files\n    tsvz\ta zip file that contains one or many tsv files\n    xls\t    a spreadsheet file format created by MS-Excel 97-2003\n    xlsx\tMS-Excel Extensions to the Office Open XML SpreadsheetML File Format.\n    xlsm\tan MS-Excel Macro-Enabled Workbook file\n    ods\t    open document spreadsheet\n    fods\tflat open document spreadsheet\n    json\tjava script object notation\n    html\thtml table of the data structure\n    simple\tsimple presentation\n    rst\t    rStructured Text presentation of the data\n    mediawiki\tmedia wiki table\n    \"\"\"\n    if isinstance(path, str):\n        path = Path(path)\n    if not isinstance(path, Path):\n        raise TypeError(\"expected pathlib path.\")\n    if not path.exists():\n        raise FileNotFoundError(str(path))\n    if delimiter is not None:\n        if not isinstance(delimiter, str):\n            raise TypeError(f\"expected str or None, not {type(delimiter)}\")\n\n    delimiters = {\n        \".csv\": \",\",\n        \".tsv\": \"\\t\",\n        \".txt\": None,\n    }\n\n    d = {}\n    if path.suffix not in delimiters:\n        try:\n            book = openpyxl.open(str(path), read_only=True)\n\n            try:\n                all_sheets = book.sheetnames\n\n                for sheet_name, sheet in ((name, book[name]) for name in all_sheets):\n                    fixup_worksheet(sheet)\n                    max_rows = min(sheet.max_row, linecount + 1)\n                    container = [None] * max_rows\n                    padding_ends = 0\n                    max_column = sheet.max_column\n\n                    for i, row_data in enumerate(sheet.iter_rows(0, header_row_index + max_rows, values_only=True), start=-header_row_index):\n                        if i < 0:\n                            # NOTE: for some reason `iter_rows` specifying a start row starts reading cells as binary, instead skip the rows that are before our first read row\n                            continue\n\n                        # NOTE: text readers do not cast types and give back strings, neither should xlsx reader, can't find documentation if it's possible to ignore this via `iter_rows` instead of casting back to string\n                        container[i] = [DataTypes.to_json(v) for v in row_data]\n\n                        for j, cell in enumerate(reversed(row_data)):\n                            if cell is None:\n                                continue\n\n                            padding_ends = max(padding_ends, max_column - j)\n\n                            break\n\n                    d[sheet_name] = [c[0:padding_ends] for c in container]\n                    d[\"delimiter\"] = None\n            finally:\n                book.close()\n\n            return d\n        except Exception:\n            pass  # it must be a raw text format.\n\n    try:\n        with path.open(\"rb\") as fi:\n            rawdata = fi.read(ENCODING_GUESS_BYTES)\n            encoding = chardet.detect(rawdata)[\"encoding\"]\n        with path.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:\n            lines = []\n            for n, line in enumerate(fi, -header_row_index):\n                if n < 0:\n                    continue\n                line = line.rstrip(\"\\n\")\n                lines.append(line)\n                if n >= linecount:\n                    break  # break on first\n\n            if delimiter is None:\n                try:\n                    d[\"delimiter\"] = delimiter = detect_seperator(\"\\n\".join(lines))\n                except ValueError as e:\n                    if e.args == (\"separator not detected\", ):\n                        d[\"delimiter\"] = delimiter = None # this will handle the case of 1 column, 1 row\n                    else:\n                        raise e\n\n            if delimiter is None:\n                d[\"delimiter\"] = delimiter = delimiters[path.suffix]  # pickup the default one\n                d[\"is_empty\"] = True  # mark as empty to return an empty table instead of throwing\n\n            text_escape = TextEscape(text_qualifier=text_qualifier, delimiter=delimiter)\n\n            d[path.name] = [text_escape(line) for line in lines]\n        return d\n    except Exception:\n        raise ValueError(f\"can't read {path.suffix}\")\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_encoding","title":"tablite.file_reader_utils.get_encoding(path, nbytes=ENCODING_GUESS_BYTES)","text":"Source code in tablite/file_reader_utils.py
def get_encoding(path, nbytes=ENCODING_GUESS_BYTES):\n    nbytes = min(nbytes, path.stat().st_size)\n    with path.open(\"rb\") as fi:\n        rawdata = fi.read(nbytes)\n        encoding = chardet.detect(rawdata)[\"encoding\"]\n        if encoding == \"ascii\":  # utf-8 is backwards compatible with ascii\n            return \"utf-8\"  # --   so should the first 10k chars not be enough,\n        return encoding  # --      the utf-8 encoding will still get it right.\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_delimiter","title":"tablite.file_reader_utils.get_delimiter(path, encoding)","text":"Source code in tablite/file_reader_utils.py
def get_delimiter(path, encoding):\n    with path.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:\n        lines = []\n        for n, line in enumerate(fi):\n            line = line.rstrip(\"\\n\")\n            lines.append(line)\n            if n > 10:\n                break  # break on first\n        delimiter = detect_seperator(\"\\n\".join(lines))\n        if delimiter is None:\n            raise ValueError(\"Delimiter could not be determined\")\n        return delimiter\n
"},{"location":"reference/groupby_utils/","title":"Groupby utils","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils","title":"tablite.groupby_utils","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils-classes","title":"Classes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupbyFunction","title":"tablite.groupby_utils.GroupbyFunction","text":"

Bases: object

"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit","title":"tablite.groupby_utils.Limit()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = None\n    self.f = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.value","title":"tablite.groupby_utils.Limit.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.f","title":"tablite.groupby_utils.Limit.f = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.update","title":"tablite.groupby_utils.Limit.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max","title":"tablite.groupby_utils.Max()","text":"

Bases: Limit

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n    self.f = max\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.value","title":"tablite.groupby_utils.Max.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.f","title":"tablite.groupby_utils.Max.f = max instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.update","title":"tablite.groupby_utils.Max.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min","title":"tablite.groupby_utils.Min()","text":"

Bases: Limit

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n    self.f = min\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.value","title":"tablite.groupby_utils.Min.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.f","title":"tablite.groupby_utils.Min.f = min instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.update","title":"tablite.groupby_utils.Min.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum","title":"tablite.groupby_utils.Sum()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum.value","title":"tablite.groupby_utils.Sum.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum.update","title":"tablite.groupby_utils.Sum.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (type(None), date, time, datetime, str)):\n        raise ValueError(f\"Sum of {type(value)} doesn't make sense.\")\n    self.value += value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product","title":"tablite.groupby_utils.Product()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self) -> None:\n    self.value = 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product.value","title":"tablite.groupby_utils.Product.value = 1 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product.update","title":"tablite.groupby_utils.Product.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value *= value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.First","title":"tablite.groupby_utils.First()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = self.empty\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.First-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.empty","title":"tablite.groupby_utils.First.empty = (None) class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.value","title":"tablite.groupby_utils.First.value = self.empty instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.update","title":"tablite.groupby_utils.First.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if self.value is First.empty:\n        self.value = value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last","title":"tablite.groupby_utils.Last()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last.value","title":"tablite.groupby_utils.Last.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last.update","title":"tablite.groupby_utils.Last.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value = value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count","title":"tablite.groupby_utils.Count()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count.value","title":"tablite.groupby_utils.Count.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count.update","title":"tablite.groupby_utils.Count.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique","title":"tablite.groupby_utils.CountUnique()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.items = set()\n    self.value = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.items","title":"tablite.groupby_utils.CountUnique.items = set() instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.value","title":"tablite.groupby_utils.CountUnique.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.update","title":"tablite.groupby_utils.CountUnique.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.items.add(value)\n    self.value = len(self.items)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average","title":"tablite.groupby_utils.Average()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.sum = 0\n    self.count = 0\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.sum","title":"tablite.groupby_utils.Average.sum = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.count","title":"tablite.groupby_utils.Average.count = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.value","title":"tablite.groupby_utils.Average.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.update","title":"tablite.groupby_utils.Average.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (date, time, datetime, str)):\n        raise ValueError(f\"Sum of {type(value)} doesn't make sense.\")\n    if value is not None:\n        self.sum += value\n        self.count += 1\n        self.value = self.sum / self.count\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation","title":"tablite.groupby_utils.StandardDeviation()","text":"

Bases: GroupbyFunction

Uses J.P. Welfords (1962) algorithm. For details see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.count = 0\n    self.mean = 0\n    self.c = 0.0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.count","title":"tablite.groupby_utils.StandardDeviation.count = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.mean","title":"tablite.groupby_utils.StandardDeviation.mean = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.c","title":"tablite.groupby_utils.StandardDeviation.c = 0.0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.value","title":"tablite.groupby_utils.StandardDeviation.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.update","title":"tablite.groupby_utils.StandardDeviation.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (date, time, datetime, str)):\n        raise ValueError(f\"Std.dev. of {type(value)} doesn't make sense.\")\n    if value is not None:\n        self.count += 1\n        dt = value - self.mean\n        self.mean += dt / self.count\n        self.c += dt * (value - self.mean)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram","title":"tablite.groupby_utils.Histogram()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.hist = defaultdict(int)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram.hist","title":"tablite.groupby_utils.Histogram.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram.update","title":"tablite.groupby_utils.Histogram.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median","title":"tablite.groupby_utils.Median()","text":"

Bases: Histogram

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.hist","title":"tablite.groupby_utils.Median.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.value","title":"tablite.groupby_utils.Median.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.update","title":"tablite.groupby_utils.Median.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode","title":"tablite.groupby_utils.Mode()","text":"

Bases: Histogram

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.hist","title":"tablite.groupby_utils.Mode.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.value","title":"tablite.groupby_utils.Mode.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.update","title":"tablite.groupby_utils.Mode.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy","title":"tablite.groupby_utils.GroupBy","text":"

Bases: object

"},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.max","title":"tablite.groupby_utils.GroupBy.max = Max class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.min","title":"tablite.groupby_utils.GroupBy.min = Min class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.sum","title":"tablite.groupby_utils.GroupBy.sum = Sum class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.product","title":"tablite.groupby_utils.GroupBy.product = Product class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.first","title":"tablite.groupby_utils.GroupBy.first = First class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.last","title":"tablite.groupby_utils.GroupBy.last = Last class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.count","title":"tablite.groupby_utils.GroupBy.count = Count class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.count_unique","title":"tablite.groupby_utils.GroupBy.count_unique = CountUnique class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.avg","title":"tablite.groupby_utils.GroupBy.avg = Average class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.stdev","title":"tablite.groupby_utils.GroupBy.stdev = StandardDeviation class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.median","title":"tablite.groupby_utils.GroupBy.median = Median class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.mode","title":"tablite.groupby_utils.GroupBy.mode = Mode class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.functions","title":"tablite.groupby_utils.GroupBy.functions = [Max, Min, Sum, First, Last, Product, Count, CountUnique, Average, StandardDeviation, Median, Mode] class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.function_names","title":"tablite.groupby_utils.GroupBy.function_names = {f.__name__: ffor f in functions} class-attribute instance-attribute","text":""},{"location":"reference/groupbys/","title":"Groupbys","text":""},{"location":"reference/groupbys/#tablite.groupbys","title":"tablite.groupbys","text":""},{"location":"reference/groupbys/#tablite.groupbys-classes","title":"Classes","text":""},{"location":"reference/groupbys/#tablite.groupbys-functions","title":"Functions","text":""},{"location":"reference/groupbys/#tablite.groupbys.groupby","title":"tablite.groupbys.groupby(T, keys, functions, tqdm=_tqdm, pbar=None)","text":"

keys: column names for grouping. functions: [optional] list of column names and group functions (See GroupyBy class) returns: table

Example:

>>> t = Table()\n>>> t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n>>> t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n>>> t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n>>> t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n>>> g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n>>> g.show()\n+===+===+===+======+\n| # | A | C |Sum(B)|\n|row|int|int| int  |\n+---+---+---+------+\n|0  |  1|  6|     2|\n|1  |  1|  5|     4|\n|2  |  2|  4|     6|\n|3  |  2|  3|     8|\n|4  |  3|  2|    10|\n|5  |  3|  1|    12|\n+===+===+===+======+\n

Cheat sheet:

list of unique values

>>> g1 = t.groupby(keys=['A'], functions=[])\n>>> g1['A'][:]\n[1,2,3]\n

alternatively:

>>> t['A'].unique()\n[1,2,3]\n

list of unique values, grouped by longest combination.

>>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n>>> g2['A'][:], g2['B'][:]\n([1,1,2,2,3,3], [1,2,3,4,5,6])\n

alternatively use:

>>> list(zip(*t.index('A', 'B').keys()))\n[(1,1,2,2,3,3) (1,2,3,4,5,6)]\n

A key (unique values) and count hereof.

>>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n>>> g3['A'][:], g3['Count(A)'][:]\n([1,2,3], [4,4,4])\n

alternatively use:

>>> t['A'].histogram()\n([1,2,3], [4,4,4])\n

for more examples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py

Source code in tablite/groupbys.py
def groupby(\n    T, keys, functions, tqdm=_tqdm, pbar=None\n):  # TODO: This is single core code.\n    \"\"\"\n    keys: column names for grouping.\n    functions: [optional] list of column names and group functions (See GroupyBy class)\n    returns: table\n\n    Example:\n    ```\n    >>> t = Table()\n    >>> t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n    >>> t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n    >>> t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n    >>> t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n    >>> g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n    >>> g.show()\n    +===+===+===+======+\n    | # | A | C |Sum(B)|\n    |row|int|int| int  |\n    +---+---+---+------+\n    |0  |  1|  6|     2|\n    |1  |  1|  5|     4|\n    |2  |  2|  4|     6|\n    |3  |  2|  3|     8|\n    |4  |  3|  2|    10|\n    |5  |  3|  1|    12|\n    +===+===+===+======+\n    ```\n\n    Cheat sheet:\n\n    list of unique values\n    ```\n    >>> g1 = t.groupby(keys=['A'], functions=[])\n    >>> g1['A'][:]\n    [1,2,3]\n    ```\n    alternatively:\n    ```\n    >>> t['A'].unique()\n    [1,2,3]\n    ```\n    list of unique values, grouped by longest combination.\n    ```\n    >>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n    >>> g2['A'][:], g2['B'][:]\n    ([1,1,2,2,3,3], [1,2,3,4,5,6])\n    ```\n    alternatively use:\n    ```\n    >>> list(zip(*t.index('A', 'B').keys()))\n    [(1,1,2,2,3,3) (1,2,3,4,5,6)]\n    ```\n\n    A key (unique values) and count hereof.\n    ```\n    >>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n    >>> g3['A'][:], g3['Count(A)'][:]\n    ([1,2,3], [4,4,4])\n    ```\n    alternatively use:\n    ```\n    >>> t['A'].histogram()\n    ([1,2,3], [4,4,4])\n    ```\n    for more examples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py\n\n    \"\"\"\n    if not isinstance(keys, list):\n        raise TypeError(\"expected keys as a list of column names\")\n\n    if keys:\n        if len(set(keys)) != len(keys):\n            duplicates = [k for k in keys if keys.count(k) > 1]\n            s = \"\" if len(duplicates) > 1 else \"s\"\n            raise ValueError(\n                f\"duplicate key{s} found across rows and columns: {duplicates}\"\n            )\n\n    if not isinstance(functions, list):\n        raise TypeError(\n            f\"Expected functions to be a list of tuples. Got {type(functions)}\"\n        )\n\n    if not keys + functions:\n        raise ValueError(\"No keys or functions?\")\n\n    if not all(len(i) == 2 for i in functions):\n        raise ValueError(\n            f\"Expected each tuple in functions to be of length 2. \\nGot {functions}\"\n        )\n\n    if not all(isinstance(a, str) for a, _ in functions):\n        L = [(a, type(a)) for a, _ in functions if not isinstance(a, str)]\n        raise ValueError(\n            f\"Expected column names in functions to be strings. Found: {L}\"\n        )\n\n    if not all(\n        issubclass(b, GroupbyFunction) and b in GroupBy.functions for _, b in functions\n    ):\n        L = [b for _, b in functions if b not in GroupBy._functions]\n        if len(L) == 1:\n            singular = f\"function {L[0]} is not in GroupBy.functions\"\n            raise ValueError(singular)\n        else:\n            plural = f\"the functions {L} are not in GroupBy.functions\"\n            raise ValueError(plural)\n\n    # only keys will produce unique values for each key group.\n    if keys and not functions:\n        cols = list(zip(*T.index(*keys)))\n        result = T.__class__()\n\n        pbar = tqdm(total=len(keys), desc=\"groupby\") if pbar is None else pbar\n\n        for col_name, col in zip(keys, cols):\n            result[col_name] = col\n\n            pbar.update(1)\n        return result\n\n    # grouping is required...\n    # 1. Aggregate data.\n    aggregation_functions = defaultdict(dict)\n    cols = keys + [col_name for col_name, _ in functions]\n    seen, L = set(), []\n    for c in cols:  # maintains order of appearance.\n        if c not in seen:\n            seen.add(c)\n            L.append(c)\n\n    # there's a table of values.\n    data = T[L]\n    if isinstance(data, Column):\n        tbl = Table()\n        tbl[L[0]] = data\n    else:\n        tbl = data\n\n    pbar = (\n        tqdm(desc=\"groupby\", total=len(tbl), disable=Config.TQDM_DISABLE)\n        if pbar is None\n        else pbar\n    )\n\n    for row in tbl.rows:\n        d = {col_name: value for col_name, value in zip(L, row)}\n        key = tuple([d[k] for k in keys])\n        agg_functions = aggregation_functions.get(key)\n        if not agg_functions:\n            aggregation_functions[key] = agg_functions = [\n                (col_name, f()) for col_name, f in functions\n            ]\n        for col_name, f in agg_functions:\n            f.update(d[col_name])\n\n        pbar.update(1)\n\n    # 2. make dense table.\n    cols = [[] for _ in cols]\n    for key_tuple, funcs in aggregation_functions.items():\n        for ix, key_value in enumerate(key_tuple):\n            cols[ix].append(key_value)\n        for ix, (_, f) in enumerate(funcs, start=len(keys)):\n            cols[ix].append(f.value)\n\n    new_names = keys + [f\"{f.__name__}({col_name})\" for col_name, f in functions]\n    result = type(T)()  # New Table.\n    for ix, (col_name, data) in enumerate(zip(new_names, cols)):\n        revised_name = unique_name(col_name, result.columns)\n        result[revised_name] = data\n    return result\n
"},{"location":"reference/import_utils/","title":"Import utils","text":""},{"location":"reference/import_utils/#tablite.import_utils","title":"tablite.import_utils","text":""},{"location":"reference/import_utils/#tablite.import_utils-attributes","title":"Attributes","text":""},{"location":"reference/import_utils/#tablite.import_utils.file_readers","title":"tablite.import_utils.file_readers = {'fods': excel_reader, 'json': excel_reader, 'html': from_html, 'hdf5': from_hdf5, 'simple': excel_reader, 'rst': excel_reader, 'mediawiki': excel_reader, 'xlsx': excel_reader, 'xls': excel_reader, 'xlsm': excel_reader, 'csv': text_reader, 'tsv': text_reader, 'txt': text_reader, 'ods': ods_reader} module-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.valid_readers","title":"tablite.import_utils.valid_readers = ','.join(list(file_readers.keys())) module-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils-classes","title":"Classes","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig","title":"tablite.import_utils.TRconfig(source, destination, start, end, guess_datatypes, delimiter, text_qualifier, text_escape_openings, text_escape_closures, strip_leading_and_tailing_whitespace, encoding, newline_offsets, fields)","text":"

Bases: object

Source code in tablite/import_utils.py
def __init__(\n    self,\n    source,\n    destination,\n    start,\n    end,\n    guess_datatypes,\n    delimiter,\n    text_qualifier,\n    text_escape_openings,\n    text_escape_closures,\n    strip_leading_and_tailing_whitespace,\n    encoding,\n    newline_offsets,\n    fields\n) -> None:\n    self.source = source\n    self.destination = destination\n    self.start = start\n    self.end = end\n    self.guess_datatypes = guess_datatypes\n    self.delimiter = delimiter\n    self.text_qualifier = text_qualifier\n    self.text_escape_openings = text_escape_openings\n    self.text_escape_closures = text_escape_closures\n    self.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace\n    self.encoding = encoding\n    self.newline_offsets = newline_offsets\n    self.fields = fields\n    type_check(start, int),\n    type_check(end, int),\n    type_check(delimiter, str),\n    type_check(text_qualifier, (str, type(None))),\n    type_check(text_escape_openings, str),\n    type_check(text_escape_closures, str),\n    type_check(encoding, str),\n    type_check(strip_leading_and_tailing_whitespace, bool),\n    type_check(newline_offsets, list)\n    type_check(fields, dict)\n
"},{"location":"reference/import_utils/#tablite.import_utils.TRconfig-attributes","title":"Attributes","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.source","title":"tablite.import_utils.TRconfig.source = source instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.destination","title":"tablite.import_utils.TRconfig.destination = destination instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.start","title":"tablite.import_utils.TRconfig.start = start instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.end","title":"tablite.import_utils.TRconfig.end = end instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.guess_datatypes","title":"tablite.import_utils.TRconfig.guess_datatypes = guess_datatypes instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.delimiter","title":"tablite.import_utils.TRconfig.delimiter = delimiter instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_qualifier","title":"tablite.import_utils.TRconfig.text_qualifier = text_qualifier instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_escape_openings","title":"tablite.import_utils.TRconfig.text_escape_openings = text_escape_openings instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_escape_closures","title":"tablite.import_utils.TRconfig.text_escape_closures = text_escape_closures instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.strip_leading_and_tailing_whitespace","title":"tablite.import_utils.TRconfig.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.encoding","title":"tablite.import_utils.TRconfig.encoding = encoding instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.newline_offsets","title":"tablite.import_utils.TRconfig.newline_offsets = newline_offsets instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.fields","title":"tablite.import_utils.TRconfig.fields = fields instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig-functions","title":"Functions","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.copy","title":"tablite.import_utils.TRconfig.copy()","text":"Source code in tablite/import_utils.py
def copy(self):\n    return TRconfig(**self.dict())\n
"},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.dict","title":"tablite.import_utils.TRconfig.dict()","text":"Source code in tablite/import_utils.py
def dict(self):\n    return {k: v for k, v in self.__dict__.items() if not (k.startswith(\"_\") or callable(v))}\n
"},{"location":"reference/import_utils/#tablite.import_utils-functions","title":"Functions","text":""},{"location":"reference/import_utils/#tablite.import_utils.from_pandas","title":"tablite.import_utils.from_pandas(T, df)","text":"

Creates Table using pd.to_dict('list')

similar to:

import pandas as pd df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]}) df a b 0 1 4 1 2 5 2 3 6 df.to_dict('list')

t = Table.from_dict(df.to_dict('list)) t.show() +===+===+===+ | # | a | b | |row|int|int| +---+---+---+ | 0 | 1| 4| | 1 | 2| 5| | 2 | 3| 6| +===+===+===+

Source code in tablite/import_utils.py
def from_pandas(T, df):\n    \"\"\"\n    Creates Table using pd.to_dict('list')\n\n    similar to:\n    >>> import pandas as pd\n    >>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n    >>> df\n        a  b\n        0  1  4\n        1  2  5\n        2  3  6\n    >>> df.to_dict('list')\n    {'a': [1, 2, 3], 'b': [4, 5, 6]}\n\n    >>> t = Table.from_dict(df.to_dict('list))\n    >>> t.show()\n        +===+===+===+\n        | # | a | b |\n        |row|int|int|\n        +---+---+---+\n        | 0 |  1|  4|\n        | 1 |  2|  5|\n        | 2 |  3|  6|\n        +===+===+===+\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    return T(columns=df.to_dict(\"list\"))  # noqa\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_hdf5","title":"tablite.import_utils.from_hdf5(T, path, tqdm=_tqdm, pbar=None)","text":"

imports an exported hdf5 table.

Note that some loss of type information is to be expected in columns of mixed type:

t.show(dtype=True) +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|str |mixed| bool| datetime | date | time | timedelta |str| int |float|int| +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1| |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1|1000|1 | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ t.to_hdf5(filename) t2 = Table.from_hdf5(filename) t2.show(dtype=True) +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|mixed|mixed| bool| datetime | datetime | time | str |str| int |float|int| +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1| 1000| 1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+

Source code in tablite/import_utils.py
def from_hdf5(T, path, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    imports an exported hdf5 table.\n\n    Note that some loss of type information is to be expected in columns of mixed type:\n    >>> t.show(dtype=True)\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  | D  |  E  |  F  |         G         |    H     |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|str |mixed| bool|      datetime     |   date   |  time  |   timedelta   |str|           int           |float|int|\n    +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|    |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1|1000|1    | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    >>> t.to_hdf5(filename)\n    >>> t2 = Table.from_hdf5(filename)\n    >>> t2.show(dtype=True)\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  |  D  |  E  |  F  |         G         |         H         |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|mixed|mixed| bool|      datetime     |      datetime     |  time  |      str      |str|           int           |float|int|\n    +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1| 1000|    1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    import h5py\n\n    type_check(path, Path)\n    t = T()\n    with h5py.File(path, \"r\") as h5:\n        for col_name in h5.keys():\n            dset = h5[col_name]\n            arr = np.array(dset[:])\n            if arr.dtype == object:\n                arr = np.array(DataTypes.guess([v.decode(\"utf-8\") for v in arr]))\n            t[col_name] = arr\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_json","title":"tablite.import_utils.from_json(T, jsn)","text":"

Imports tables exported using .to_json

Source code in tablite/import_utils.py
def from_json(T, jsn):\n    \"\"\"\n    Imports tables exported using .to_json\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    import json\n\n    type_check(jsn, str)\n    d = json.loads(jsn)\n    return T(columns=d[\"columns\"])\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_html","title":"tablite.import_utils.from_html(T, path, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/import_utils.py
def from_html(T, path, tqdm=_tqdm, pbar=None):\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    type_check(path, Path)\n\n    if pbar is None:\n        total = path.stat().st_size\n        pbar = tqdm(total=total, desc=\"from_html\", disable=Config.TQDM_DISABLE)\n\n    row_start, row_end = \"<tr>\", \"</tr>\"\n    value_start, value_end = \"<th>\", \"</th>\"\n    chunk = \"\"\n    t = None  # will be T()\n    start, end = 0, 0\n    data = {}\n    with path.open(\"r\") as fi:\n        while True:\n            start = chunk.find(row_start, start)  # row tag start\n            end = chunk.find(row_end, end)  # row tag end\n            if start == -1 or end == -1:\n                new = fi.read(100_000)\n                pbar.update(len(new))\n                if new == \"\":\n                    break\n                chunk += new\n                continue\n            # get indices from chunk\n            row = chunk[start + len(row_start) : end]\n            fields = [v.rstrip(value_end) for v in row.split(value_start)]\n            if not data:\n                headers = fields[:]\n                data = {f: [] for f in headers}\n                continue\n            else:\n                for field, header in zip(fields, headers):\n                    data[header].append(field)\n\n            chunk = chunk[end + len(row_end) :]\n\n            if len(data[headers[0]]) == Config.PAGE_SIZE:\n                if t is None:\n                    t = T(columns=data)\n                else:\n                    for k, v in data.items():\n                        t[k].extend(DataTypes.guess(v))\n                data = {f: [] for f in headers}\n\n    for k, v in data.items():\n        t[k].extend(DataTypes.guess(v))\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.excel_reader","title":"tablite.import_utils.excel_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, tqdm=_tqdm, **kwargs)","text":"

returns Table from excel

**kwargs are excess arguments that are ignored.

Source code in tablite/import_utils.py
def excel_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, tqdm=_tqdm, **kwargs):\n    \"\"\"\n    returns Table from excel\n\n    **kwargs are excess arguments that are ignored.\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    book = openpyxl.load_workbook(path, read_only=True, data_only=True)\n\n    if sheet is None:  # help the user.\n        sheet_list = ', '.join((f'\\n - {c}' for c in book.sheetnames))\n        raise ValueError(f\"No 'sheet' declared, available sheets:{sheet_list}\")\n    elif sheet not in book.sheetnames:\n        raise ValueError(f\"sheet not found: {sheet}\")\n\n    if not (isinstance(start, int) and start >= 0):\n        raise ValueError(\"expected start as an integer >=0\")\n    if not (isinstance(limit, int) and limit > 0):\n        raise ValueError(\"expected limit as integer > 0\")\n\n    worksheet = book[sheet]\n    fixup_worksheet(worksheet)\n\n    try:\n        # get the first row to know our headers or the number of columns\n        fields = [str(c.value) for c in next(worksheet.iter_rows(min_row=header_row_index + 1))] # excel is offset by 1\n    except StopIteration:\n        # excel was empty, return empty table\n        return T()\n\n    if not first_row_has_headers:\n        # since the first row did not contain headers, we use the column count to populate header names\n        fields = [str(i) for i in range(len(fields))]\n\n    if columns is None:\n        # no columns were specified by user to import, that means we import all of the them\n        columns = []\n\n        for f in fields:\n            # fixup the duplicate column names\n            columns.append(unique_name(f, columns))\n\n        field_dict = {k: i for i, k in enumerate(columns)}\n    else:\n        field_dict = {}\n\n        for k, i in ((k, fields.index(k)) for k in columns):\n            # fixup the duplicate column names\n            field_dict[unique_name(k, field_dict.keys())] = i\n\n    # calculate our data rows iterator offset\n    it_offset = start + (1 if first_row_has_headers else 0) + header_row_index + 1\n\n    # attempt to fetch number of rows in the sheet\n    total_rows = worksheet.max_row\n    real_tqdm = True\n\n    if total_rows is None:\n        # i don't know what causes it but max_row can be None in some cases, so we don't know how large the dataset is\n        total_rows = it_offset + limit\n        real_tqdm = False\n\n    # create the actual data rows iterator\n    it_rows = worksheet.iter_rows(min_row=it_offset, max_row=min(it_offset+limit, total_rows))\n    it_used_indices = list(field_dict.values())\n\n    # filter columns that we're not going to use\n    it_rows_filtered = ([row[idx].value for idx in it_used_indices] for row in it_rows)\n\n    # create page directory\n    workdir = Path(Config.workdir) / Config.pid\n    pagesdir = workdir/\"pages\"\n    pagesdir.mkdir(exist_ok=True, parents=True)\n\n    field_names = list(field_dict.keys())\n    column_count = len(field_names)\n\n    page_fhs = None\n\n    # prepopulate the table with columns\n    table = T()\n    for name in field_names:\n        table[name] = Column(table.path)\n\n    pbar_fname = path.name\n    if len(pbar_fname) > 20:\n        pbar_fname = pbar_fname[0:10] + \"...\" + pbar_fname[-7:]\n\n    if real_tqdm:\n        # we can create a true tqdm progress bar, make one\n        tqdm_iter = tqdm(it_rows_filtered, total=total_rows, desc=f\"importing excel: {pbar_fname}\")\n    else:\n        \"\"\"\n            openpyxls was unable to precalculate the size of the excel for whatever reason\n            forcing recalc would require parsing entire file\n            drop the progress bar in that case, just show iterations\n\n            as an alternative we can use \u03a3=1/x but it just doesn't look good, show iterations per second instead\n        \"\"\"\n        tqdm_iter = tqdm(it_rows_filtered, desc=f\"importing excel: {pbar_fname}\")\n\n    tqdm_iter = enumerate(tqdm_iter)\n\n    while True:\n        try:\n            idx, row = next(tqdm_iter)\n        except StopIteration:\n            break # because in some cases we can't know the size of excel to set the upper iterator limit we loop until stop iteration is encountered\n\n        if idx % Config.PAGE_SIZE == 0:\n            if page_fhs is not None:\n                # we reached the max page file size, fix the pages\n                [_fix_xls_page(table, c, fh) for c, fh in zip(field_names, page_fhs)]\n\n            page_fhs = [None] * column_count\n\n            for cidx in range(column_count):\n                # allocate new pages\n                pg_path = pagesdir / f\"{next(Page.ids)}.npy\"\n                page_fhs[cidx] = open(pg_path, \"wb\")\n\n        for fh, value in zip(page_fhs, row):\n            \"\"\"\n                since excel types are already cast into appropriate type we're going to do two passes per page\n\n                we create our temporary custom format:\n                packed type|packed byte count|packed bytes|...\n\n                available types:\n                    * q - int64\n                    * d - float64\n                    * s - string\n                    * b - boolean\n                    * n - none\n                    * p - pickled (date, time, datetime)\n            \"\"\"\n            dtype = type(value)\n\n            if dtype == int:\n                ptype, bytes_ = b'q', struct.pack('q', value) # pack int as int64\n            elif dtype == float:\n                ptype, bytes_ = b'd', struct.pack('d', value) # pack float as float64\n            elif dtype == str:\n                ptype, bytes_ = b's', value.encode(\"utf-8\")   # pack string\n            elif dtype == bool:\n                ptype, bytes_ = b'b', b'1' if value else b'0' # pack boolean\n            elif value is None:\n                ptype, bytes_ = b'n', b''                     # pack none\n            elif dtype in [date, time, datetime]:\n                ptype, bytes_ = b'p', pkl.dumps(value)        # pack object types via pickle\n            else:\n                raise NotImplementedError()\n\n            byte_count = struct.pack('I', len(bytes_))        # pack our payload size, i doubt payload size can be over uint32\n\n            # dump object to file\n            fh.write(ptype)\n            fh.write(byte_count)\n            fh.write(bytes_)\n\n    if page_fhs is not None:\n        # we reached end of the loop, fix the pages\n        [_fix_xls_page(table, c, fh) for c, fh in zip(field_names, page_fhs)]\n\n    return table\n
"},{"location":"reference/import_utils/#tablite.import_utils.ods_reader","title":"tablite.import_utils.ods_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, **kwargs)","text":"

returns Table from .ODS

Source code in tablite/import_utils.py
def ods_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, **kwargs):\n    \"\"\"\n    returns Table from .ODS\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    sheets = pyexcel.get_book_dict(file_name=str(path))\n\n    if sheet is None or sheet not in sheets:\n        raise ValueError(f\"No sheet_name declared: \\navailable sheets:\\n{[s.name for s in sheets]}\")\n\n    data = sheets[sheet]\n    for _ in range(len(data)):  # remove empty lines at the end of the data.\n        if \"\" == \"\".join(str(i) for i in data[-1]):\n            data = data[:-1]\n        else:\n            break\n\n    if not (isinstance(start, int) and start >= 0):\n        raise ValueError(\"expected start as an integer >=0\")\n    if not (isinstance(limit, int) and limit > 0):\n        raise ValueError(\"expected limit as integer > 0\")\n\n    t = T()\n\n    used_columns_names = set()\n    for ix, value in enumerate(data[header_row_index]):\n        if first_row_has_headers:\n            header, start_row_pos = str(value), (1 + header_row_index)\n        else:\n            header, start_row_pos = f\"_{ix + 1}\", (0 + header_row_index)\n\n        if columns is not None:\n            if header not in columns:\n                continue\n\n        unique_column_name = unique_name(str(header), used_columns_names)\n        used_columns_names.add(unique_column_name)\n\n        t[unique_column_name] = [row[ix] for row in data[start_row_pos : start_row_pos + limit] if len(row) > ix]\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.text_reader_task","title":"tablite.import_utils.text_reader_task(source, destination, start, end, guess_datatypes, delimiter, text_qualifier, text_escape_openings, text_escape_closures, strip_leading_and_tailing_whitespace, encoding, newline_offsets, fields)","text":"

PARALLEL TASK FUNCTION reads columnsname + path[start:limit] into hdf5.

source: csv or txt file destination: filename for page. start: int: start of page. end: int: end of page. guess_datatypes: bool: if True datatypes will be inferred by datatypes.Datatypes.guess delimiter: ',' ';' or '|' text_qualifier: str: commonly \" text_escape_openings: str: default: \"({[ text_escape_closures: str: default: ]})\" strip_leading_and_tailing_whitespace: bool encoding: chardet encoding ('utf-8, 'ascii', ..., 'ISO-22022-CN')

Source code in tablite/import_utils.py
def text_reader_task(\n    source,\n    destination,\n    start,\n    end,\n    guess_datatypes,\n    delimiter,\n    text_qualifier,\n    text_escape_openings,\n    text_escape_closures,\n    strip_leading_and_tailing_whitespace,\n    encoding,\n    newline_offsets,\n    fields\n):\n    \"\"\"PARALLEL TASK FUNCTION\n    reads columnsname + path[start:limit] into hdf5.\n\n    source: csv or txt file\n    destination: filename for page.\n    start: int: start of page.\n    end: int: end of page.\n    guess_datatypes: bool: if True datatypes will be inferred by datatypes.Datatypes.guess\n    delimiter: ',' ';' or '|'\n    text_qualifier: str: commonly \\\"\n    text_escape_openings: str: default: \"({[\n    text_escape_closures: str: default: ]})\"\n    strip_leading_and_tailing_whitespace: bool\n    encoding: chardet encoding ('utf-8, 'ascii', ..., 'ISO-22022-CN')\n    \"\"\"\n    if isinstance(source, str):\n        source = Path(source)\n    type_check(source, Path)\n    if not source.exists():\n        raise FileNotFoundError(f\"File not found: {source}\")\n    type_check(destination, list)\n\n    # declare CSV dialect.\n    delim = delimiter\n\n    class Dialect(csv.Dialect):\n        delimiter = delim\n        quotechar = '\"' if text_qualifier is None else text_qualifier\n        escapechar = '\\\\'\n        doublequote = True\n        quoting = csv.QUOTE_MINIMAL\n        skipinitialspace = False if strip_leading_and_tailing_whitespace is None else strip_leading_and_tailing_whitespace\n        lineterminator = \"\\n\"\n\n    with source.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:  # --READ\n        fi.seek(newline_offsets[start])\n        reader = csv.reader(fi, dialect=Dialect)\n\n        # if there's an issue with file handlers on windows, we can make a special case for windows where the file is opened on demand and appended instead of opening all handlers at once\n        page_file_handlers = [open(f, mode=\"wb\") for f in destination]\n\n        # identify longest str\n        longest_str = [1 for _ in range(len(destination))]\n        for row in (next(reader) for _ in range(end - start)):\n            for idx, c in ((fields[idx], c) for idx, c in filter(lambda t: t[0] in fields, enumerate(row))):\n                longest_str[idx] = max(longest_str[idx], len(c))\n\n        column_formats = [f\"<U{i}\" for i in longest_str]\n        for idx, cf in enumerate(column_formats):\n            _create_numpy_header(cf, (end - start, ), page_file_handlers[idx])\n\n        # write page arrays to files\n        fi.seek(newline_offsets[start])\n        for row in (next(reader) for _ in range(end - start)):\n            for idx, c in ((fields[idx], c) for idx, c in filter(lambda t: t[0] in fields, enumerate(row))):\n                cbytes = np.asarray(c, dtype=column_formats[idx]).tobytes()\n                page_file_handlers[idx].write(cbytes)\n\n        [phf.close() for phf in page_file_handlers]\n
"},{"location":"reference/import_utils/#tablite.import_utils.text_reader","title":"tablite.import_utils.text_reader(T, path, columns, first_row_has_headers, header_row_index, encoding, start, limit, newline, guess_datatypes, text_qualifier, strip_leading_and_tailing_whitespace, delimiter, text_escape_openings, text_escape_closures, tqdm=_tqdm, **kwargs)","text":"Source code in tablite/import_utils.py
def text_reader(\n    T,\n    path,\n    columns,\n    first_row_has_headers,\n    header_row_index,\n    encoding,\n    start,\n    limit,\n    newline,\n    guess_datatypes,\n    text_qualifier,\n    strip_leading_and_tailing_whitespace,\n    delimiter,\n    text_escape_openings,\n    text_escape_closures,\n    tqdm=_tqdm,\n    **kwargs,\n):\n    if encoding is None:\n        encoding = get_encoding(path, nbytes=ENCODING_GUESS_BYTES)\n\n    if encoding.lower() in [\"utf8\", \"utf-8\", \"utf-8-sig\"]:\n        enc = \"ENC_UTF8\"\n    elif encoding.lower() in [\"utf16\", \"utf-16\"]:\n        enc = \"ENC_UTF16\"\n    elif encoding in Config.NIM_SUPPORTED_CONV_TYPES:\n        enc = f\"ENC_CONV|{encoding}\"\n    else:\n        raise NotImplementedError(f\"encoding not implemented: {encoding}\")\n\n    pid = Config.workdir / Config.pid\n    kwargs = {}\n\n    if first_row_has_headers is not None:\n        kwargs[\"first_row_has_headers\"] = first_row_has_headers\n    if header_row_index is not None:\n        kwargs[\"header_row_index\"] = header_row_index\n    if columns is not None:\n        kwargs[\"columns\"] = columns\n    if start is not None:\n        kwargs[\"start\"] = start\n    if limit is not None and limit != sys.maxsize:\n        kwargs[\"limit\"] = limit\n    if guess_datatypes is not None:\n        kwargs[\"guess_datatypes\"] = guess_datatypes\n    if newline is not None:\n        kwargs[\"newline\"] = newline\n    if delimiter is not None:\n        kwargs[\"delimiter\"] = delimiter\n    if text_qualifier is not None:\n        kwargs[\"text_qualifier\"] = text_qualifier\n        kwargs[\"quoting\"] = \"QUOTE_MINIMAL\"\n    else:\n        kwargs[\"quoting\"] = \"QUOTE_NONE\"\n    if strip_leading_and_tailing_whitespace is not None:\n        kwargs[\"strip_leading_and_tailing_whitespace\"] = strip_leading_and_tailing_whitespace\n\n    return nimlite.text_reader(\n        T, pid, path, enc,\n        **kwargs,\n        tqdm=tqdm\n    )\n
"},{"location":"reference/import_utils/#tablite.import_utils-modules","title":"Modules","text":""},{"location":"reference/imputation/","title":"Imputation","text":""},{"location":"reference/imputation/#tablite.imputation","title":"tablite.imputation","text":""},{"location":"reference/imputation/#tablite.imputation-classes","title":"Classes","text":""},{"location":"reference/imputation/#tablite.imputation-functions","title":"Functions","text":""},{"location":"reference/imputation/#tablite.imputation.imputation","title":"tablite.imputation.imputation(T, targets, missing=None, method='carry forward', sources=None, tqdm=_tqdm, pbar=None)","text":"

In statistics, imputation is the process of replacing missing data with substituted values.

See more: https://en.wikipedia.org/wiki/Imputation_(statistics)

PARAMETER DESCRIPTION table

source table.

TYPE: Table

targets

column names to find and replace missing values

TYPE: str or list of strings

missing

values to be replaced.

TYPE: None or iterable DEFAULT: None

method

method to be used for replacement. Options:

'carry forward': takes the previous value, and carries forward into fields where values are missing. +: quick. Realistic on time series. -: Can produce strange outliers.

'mean': calculates the column mean (exclude missing) and copies the mean in as replacement. +: quick -: doesn't work on text. Causes data set to drift towards the mean.

'mode': calculates the column mode (exclude missing) and copies the mean in as replacement. +: quick -: most frequent value becomes over-represented in the sample

'nearest neighbour': calculates normalised distance between items in source columns selects nearest neighbour and copies value as replacement. +: works for any datatype. -: computationally intensive (e.g. slow)

TYPE: str DEFAULT: 'carry forward'

sources

NEAREST NEIGHBOUR ONLY column names to be used during imputation. if None or empty, all columns will be used.

TYPE: list of strings DEFAULT: None

RETURNS DESCRIPTION table

table with replaced values.

Source code in tablite/imputation.py
def imputation(T, targets, missing=None, method=\"carry forward\", sources=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    In statistics, imputation is the process of replacing missing data with substituted values.\n\n    See more: https://en.wikipedia.org/wiki/Imputation_(statistics)\n\n    Args:\n        table (Table): source table.\n\n        targets (str or list of strings): column names to find and\n            replace missing values\n\n        missing (None or iterable): values to be replaced.\n\n        method (str): method to be used for replacement. Options:\n\n            'carry forward':\n                takes the previous value, and carries forward into fields\n                where values are missing.\n                +: quick. Realistic on time series.\n                -: Can produce strange outliers.\n\n            'mean':\n                calculates the column mean (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: doesn't work on text. Causes data set to drift towards the mean.\n\n            'mode':\n                calculates the column mode (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: most frequent value becomes over-represented in the sample\n\n            'nearest neighbour':\n                calculates normalised distance between items in source columns\n                selects nearest neighbour and copies value as replacement.\n                +: works for any datatype.\n                -: computationally intensive (e.g. slow)\n\n        sources (list of strings): NEAREST NEIGHBOUR ONLY\n            column names to be used during imputation.\n            if None or empty, all columns will be used.\n\n    Returns:\n        table: table with replaced values.\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if isinstance(targets, str) and targets not in T.columns:\n        targets = [targets]\n    if isinstance(targets, list):\n        for name in targets:\n            if not isinstance(name, str):\n                raise TypeError(f\"expected str, not {type(name)}\")\n            if name not in T.columns:\n                raise ValueError(f\"target item {name} not a column name in T.columns:\\n{T.columns}\")\n    else:\n        raise TypeError(\"Expected source as list of column names\")\n\n    if missing is None:\n        missing = {None}\n    else:\n        missing = set(missing)\n\n    if method == \"nearest neighbour\":\n        if sources in (None, []):\n            sources = list(T.columns)\n        if isinstance(sources, str):\n            sources = [sources]\n        if isinstance(sources, list):\n            for name in sources:\n                if not isinstance(name, str):\n                    raise TypeError(f\"expected str, not {type(name)}\")\n                if name not in T.columns:\n                    raise ValueError(f\"source item {name} not a column name in T.columns:\\n{T.columns}\")\n        else:\n            raise TypeError(\"Expected source as list of column names\")\n\n    methods = [\"nearest neighbour\", \"mean\", \"mode\", \"carry forward\"]\n\n    if method == \"carry forward\":\n        return carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None)\n    elif method in {\"mean\", \"mode\"}:\n        return stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None)\n    elif method == \"nearest neighbour\":\n        return nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None)\n    else:\n        raise ValueError(f\"method {method} not recognised amonst known methods: {list(methods)})\")\n
"},{"location":"reference/imputation/#tablite.imputation.carry_forward","title":"tablite.imputation.carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    if pbar is None:\n        total = len(targets) * len(T)\n        pbar = tqdm(total=total, desc=\"imputation.carry_forward\", disable=Config.TQDM_DISABLE)\n\n    new = type(T)()\n    for name in T.columns:\n        if name in targets:\n            data = T[name][:]  # create copy\n            last_value = None\n            for ix, v in enumerate(data):\n                if v in missing:  # perform replacement\n                    data[ix] = last_value\n                else:  # keep last value.\n                    last_value = v\n                pbar.update(1)\n            new[name] = data\n        else:\n            new[name] = T[name]\n\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation.stats_method","title":"tablite.imputation.stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    if pbar is None:\n        total = len(targets)\n        pbar = tqdm(total=total, desc=f\"imputation.{method}\", disable=Config.TQDM_DISABLE)\n\n    new = type(T)()\n    for name in T.columns:\n        if name in targets:\n            col = T.columns[name]\n            assert isinstance(col, Column)\n            stats = col.statistics()\n            new_value = stats[method]\n            col.replace(mapping={m: new_value for m in missing})\n            new[name] = col\n            pbar.update(1)\n        else:\n            new[name] = T[name]  # no entropy, keep as is.\n\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation.nearest_neighbour","title":"tablite.imputation.nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    new = T.copy()\n    norm_index = {}\n    normalised_values = Table()\n    for name in sources:\n        values = T[name].unique().tolist()\n        values = sort_utils.unix_sort(values, reverse=False)\n        values = [(v, k) for k, v in values.items()]\n        values.sort()\n        values = [k for _, k in values]\n\n        n = len([v for v in values if v not in missing])\n        d = {v: i / n if v not in missing else math.inf for i, v in enumerate(values)}\n        normalised_values[name] = [d[v] for v in T[name]]\n        norm_index[name] = d\n        values.clear()\n\n    missing_value_index = T.index(*targets)\n    missing_value_index = {k: v for k, v in missing_value_index.items() if missing.intersection(set(k))}  # strip out all that do not have missings.\n\n    ranks = set()\n    for k, v in missing_value_index.items():\n        ranks.update(set(k))\n    item_order = sort_utils.unix_sort(list(ranks))\n    new_order = {tuple(item_order[i] for i in k): k for k in missing_value_index.keys()}\n\n    if pbar is None:\n        total = total=sum(len(v) for v in missing_value_index.values())\n        pbar = tqdm(total=total, desc=f\"imputation.nearest_neighbour\", disable=Config.TQDM_DISABLE)\n\n    for _, key in sorted(new_order.items(), reverse=True):  # Fewest None's are at the front of the list.\n        for row_id in missing_value_index[key]:\n            err_map = [0.0 for _ in range(len(T))]\n            for n, v in T.to_dict(columns=sources, slice_=slice(row_id, row_id + 1, 1)).items():\n                # ^--- T.to_dict doesn't go to disk as hence saves an IO step.\n                v = v[0]\n                norm_value = norm_index[n][v]\n                if norm_value != math.inf:\n                    err_map = [e1 + abs(norm_value - e2) for e1, e2 in zip(err_map, normalised_values[n])]\n\n            min_err = min(err_map)\n            ix = err_map.index(min_err)\n\n            for name in targets:\n                current_value = new[name][row_id]\n                if current_value not in missing:  # no need to replace anything.\n                    continue\n                if new[name][ix] not in missing:  # can confidently impute.\n                    new[name][row_id] = new[name][ix]\n                else:  # replacement is required, but ix points to another missing value.\n                    # we therefore have to search after the next best match:\n                    tmp_err_map = err_map[:]\n                    for _ in range(len(err_map)):\n                        tmp_min_err = min(tmp_err_map)\n                        tmp_ix = tmp_err_map.index(tmp_min_err)\n                        if row_id == tmp_ix:\n                            tmp_err_map[tmp_ix] = math.inf\n                            continue\n                        elif new[name][tmp_ix] in missing:\n                            tmp_err_map[tmp_ix] = math.inf\n                            continue\n                        else:\n                            new[name][row_id] = new[name][tmp_ix]\n                            break\n\n            pbar.update(1)\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation-modules","title":"Modules","text":""},{"location":"reference/joins/","title":"Joins","text":""},{"location":"reference/joins/#tablite.joins","title":"tablite.joins","text":""},{"location":"reference/joins/#tablite.joins-classes","title":"Classes","text":""},{"location":"reference/joins/#tablite.joins-functions","title":"Functions","text":""},{"location":"reference/joins/#tablite.joins.join","title":"tablite.joins.join(T, other, left_keys, right_keys, left_columns, right_columns, kind='inner', tqdm=_tqdm, pbar=None)","text":"

short-cut for all join functions.

PARAMETER DESCRIPTION T

left table

TYPE: Table

other

right table

TYPE: Table

left_keys

list of keys for the join from left table.

TYPE: list

right_keys

list of keys for the join from right table.

TYPE: list

left_columns

list of columns names to retain from left table. If None, all are retained.

TYPE: list

right_columns

list of columns names to retain from right table. If None, all are retained.

TYPE: list

kind

'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".

TYPE: str DEFAULT: 'inner'

tqdm

tqdm progress counter. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

tqdm.progressbar. Defaults to None.

TYPE: pbar DEFAULT: None

RAISES DESCRIPTION ValueError

if join type is unknown.

RETURNS DESCRIPTION Table

joined table.

Source code in tablite/joins.py
def join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", tqdm=_tqdm, pbar=None):\n    \"\"\"short-cut for all join functions.\n\n    Args:\n        T (Table): left table\n        other (Table): right table\n        left_keys (list): list of keys for the join from left table.\n        right_keys (list): list of keys for the join from right table.\n        left_columns (list): list of columns names to retain from left table. \n            If None, all are retained.\n        right_columns (list): list of columns names to retain from right table. \n            If None, all are retained.\n        kind (str, optional): 'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".\n        tqdm (tqdm, optional): tqdm progress counter. Defaults to _tqdm.\n        pbar (tqdm.pbar, optional): tqdm.progressbar. Defaults to None.\n\n    Raises:\n        ValueError: if join type is unknown.\n\n    Returns:\n        Table: joined table.\n    \"\"\"\n    kinds = {\n        \"inner\": T.inner_join,\n        \"left\": T.left_join,\n        \"outer\": T.outer_join,\n        \"cross\": T.cross_join,\n    }\n    if kind not in kinds:\n        raise ValueError(f\"join type unknown: {kind}\")\n    f = kinds.get(kind, None)\n    return f(other, left_keys, right_keys, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/joins/#tablite.joins.left_join","title":"tablite.joins.left_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"PARAMETER DESCRIPTION T

left table

TYPE: Table

other

right table

TYPE: Table

left_keys

list of keys for the join from left table.

TYPE: list

right_keys

list of keys for the join from right table.

TYPE: list

left_columns

list of columns names to retain from left table. If None, all are retained.

TYPE: list DEFAULT: None

right_columns

list of columns names to retain from right table. If None, all are retained.

TYPE: list DEFAULT: None

kind

'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".

TYPE: str

tqdm

tqdm progress counter. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

tqdm.progressbar. Defaults to None.

TYPE: pbar DEFAULT: None

merge_keys

merges keys to the left, so that cases where right key is None, a key exists.

TYPE: boolean DEFAULT: None

RETURNS DESCRIPTION Table

joined table

Example:

SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n

Tablite:

>>> left_join = numbers.left_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n)\n
Source code in tablite/joins.py
def left_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    Args:\n        T (Table): left table\n        other (Table): right table\n        left_keys (list): list of keys for the join from left table.\n        right_keys (list): list of keys for the join from right table.\n        left_columns (list): list of columns names to retain from left table. \n            If None, all are retained.\n        right_columns (list): list of columns names to retain from right table. \n            If None, all are retained.\n        kind (str, optional): 'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".\n        tqdm (tqdm, optional): tqdm progress counter. Defaults to _tqdm.\n        pbar (tqdm.pbar, optional): tqdm.progressbar. Defaults to None.\n        merge_keys (boolean): merges keys to the left, so that cases where right key is None, a key exists.\n\n    Returns: \n        Table: joined table\n\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n    ```\n    Tablite: \n    ```\n    >>> left_join = numbers.left_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n    )\n    ```\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT = [], []\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, (-1,))\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)  # compress memory of python list to array.\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys is True:\n        boolean_map = (RIGHT == -1)\n        column_names = [n for n in T.columns]  # left side only.\n        for left_name,right_name in zip(left_keys,right_keys):\n            right_name = unique_name(right_name, T.columns)\n            column_names.append(right_name)\n            result = where(result, boolean_map, left_name,right_name,new=left_name)\n    return result\n
"},{"location":"reference/joins/#tablite.joins.inner_join","title":"tablite.joins.inner_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that only left key is present :return: new Table Example: SQL: SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color Tablite: inner_join = numbers.inner_join( letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'] )

Source code in tablite/joins.py
def inner_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that only left key is present\n    :return: new Table\n    Example:\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    Tablite: inner_join = numbers.inner_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT = [], []\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, None)\n        if right_ixs is None:\n            continue\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys:\n        for right_name in right_keys:\n            right_name = unique_name(right_name, T.columns)\n            if right_name in result.columns:\n                del result[right_name]\n    return result\n
"},{"location":"reference/joins/#tablite.joins.outer_join","title":"tablite.joins.outer_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that cases where a key match is None, a key exists. :return: new Table Example: SQL: SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color Tablite: outer_join = numbers.outer_join( letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'] )

Source code in tablite/joins.py
def outer_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that cases where a key match is None, a key exists.\n    :return: new Table\n    Example:\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    Tablite: outer_join = numbers.outer_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT, RIGHT_UNUSED = [], [], set(right_index.keys())\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, (-1,))\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n                RIGHT_UNUSED.discard(left_key)\n\n    for right_key in RIGHT_UNUSED:\n        for right_ix in right_index[right_key]:\n            LEFT.append(-1)\n            RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys is True:\n        boolean_map = (LEFT != -1)\n        column_names = [n for n in T.columns]\n        for left_name,right_name in zip(left_keys,right_keys):\n            right_name = unique_name(right_name, T.columns)\n            column_names.append(right_name)\n            result = where(result, boolean_map, left_name,right_name,new=left_name)\n    return result\n
"},{"location":"reference/joins/#tablite.joins.cross_join","title":"tablite.joins.cross_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that only left key is present :return: new Table

CROSS JOIN returns the Cartesian product of rows from tables in the join. In other words, it will produce rows which combine each row from the first table with each row from the second table

Source code in tablite/joins.py
def cross_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that only left key is present\n    :return: new Table\n\n    CROSS JOIN returns the Cartesian product of rows from tables in the join.\n    In other words, it will produce rows which combine each row from the first table\n    with each row from the second table\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    LEFT, RIGHT = zip(*product(range(len(T)), range(len(other))))\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys:\n        for right_name in right_keys:\n            right_name = unique_name(right_name, T.columns)\n            if right_name in result.columns:\n                del result[right_name]\n    return result\n
"},{"location":"reference/lookup/","title":"Lookup","text":""},{"location":"reference/lookup/#tablite.lookup","title":"tablite.lookup","text":""},{"location":"reference/lookup/#tablite.lookup-attributes","title":"Attributes","text":""},{"location":"reference/lookup/#tablite.lookup-classes","title":"Classes","text":""},{"location":"reference/lookup/#tablite.lookup-functions","title":"Functions","text":""},{"location":"reference/lookup/#tablite.lookup.lookup","title":"tablite.lookup.lookup(T, other, *criteria, all=True, tqdm=_tqdm)","text":"

function for looking up values in other according to criteria in ascending order. :param: T: Table :param: other: Table sorted in ascending search order. :param: criteria: Each criteria must be a tuple with value comparisons in the form: (LEFT, OPERATOR, RIGHT) :param: all: boolean: True=ALL, False=ANY

OPERATOR must be a callable that returns a boolean LEFT must be a value that the OPERATOR can compare. RIGHT must be a value that the OPERATOR can compare.

Examples:

comparison of two columns:

('column A', \"==\", 'column B')\n

compare value from column 'Date' with date 24/12.

('Date', \"<\", DataTypes.date(24,12) )\n

uses custom function to compare value from column 'text 1' with value from column 'text 2'

f = lambda L,R: all( ord(L) < ord(R) )\n('text 1', f, 'text 2')\n
Source code in tablite/lookup.py
def lookup(T, other, *criteria, all=True, tqdm=_tqdm):\n    \"\"\"function for looking up values in `other` according to criteria in ascending order.\n    :param: T: Table \n    :param: other: Table sorted in ascending search order.\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n        (LEFT, OPERATOR, RIGHT)\n    :param: all: boolean: True=ALL, False=ANY\n\n    OPERATOR must be a callable that returns a boolean\n    LEFT must be a value that the OPERATOR can compare.\n    RIGHT must be a value that the OPERATOR can compare.\n\n    Examples:\n        comparison of two columns:\n\n            ('column A', \"==\", 'column B')\n\n        compare value from column 'Date' with date 24/12.\n\n            ('Date', \"<\", DataTypes.date(24,12) )\n\n        uses custom function to compare value from column\n        'text 1' with value from column 'text 2'\n\n            f = lambda L,R: all( ord(L) < ord(R) )\n            ('text 1', f, 'text 2')\n\n    \"\"\"\n    sub_cls_check(T, Table)\n    sub_cls_check(other, Table)\n\n    all = all\n    any = not all\n\n    ops = lookup_ops\n\n    functions, left_criteria, right_criteria = [], set(), set()\n\n    for left, op, right in criteria:\n        left_criteria.add(left)\n        right_criteria.add(right)\n        if callable(op):\n            pass  # it's a custom function.\n        else:\n            op = ops.get(op, None)\n            if not callable(op):\n                raise ValueError(f\"{op} not a recognised operator for comparison.\")\n\n        functions.append((op, left, right))\n    left_columns = [n for n in left_criteria if n in T.columns]\n    right_columns = [n for n in right_criteria if n in other.columns]\n\n    result_index = np.empty(shape=(len(T)), dtype=np.int64)\n    cache = {}\n    left = T[left_columns]\n    if isinstance(left, Column):\n        tmp, left = left, Table()\n        left[left_columns[0]] = tmp\n    right = other[right_columns]\n    if isinstance(right, Column):\n        tmp, right = right, Table()\n        right[right_columns[0]] = tmp\n    assert isinstance(left, Table)\n    assert isinstance(right, Table)\n\n    for ix, row1 in tqdm(enumerate(left.rows), total=len(T), disable=Config.TQDM_DISABLE):\n        row1_tup = tuple(row1)\n        row1d = {name: value for name, value in zip(left_columns, row1)}\n        row1_hash = hash(row1_tup)\n\n        match_found = True if row1_hash in cache else False\n\n        if not match_found:  # search.\n            for row2ix, row2 in enumerate(right.rows):\n                row2d = {name: value for name, value in zip(right_columns, row2)}\n\n                evaluations = {op(row1d.get(left, left), row2d.get(right, right)) for op, left, right in functions}\n                # The evaluations above does a neat trick:\n                # as L is a dict, L.get(left, L) will return a value\n                # from the columns IF left is a column name. If it isn't\n                # the function will treat left as a value.\n                # The same applies to right.\n                all_ = all and (False not in evaluations)\n                any_ = any and True in evaluations\n                if all_ or any_:\n                    match_found = True\n                    cache[row1_hash] = row2ix\n                    break\n\n        if not match_found:  # no match found.\n            cache[row1_hash] = -1  # -1 is replacement for None in the index as numpy can't handle Nones.\n\n        result_index[ix] = cache[row1_hash]\n\n    f = select_processing_method(2 * max(len(T), len(other)), _sp_lookup, _mp_lookup)\n    return f(T, other, result_index)\n
"},{"location":"reference/match/","title":"Match","text":""},{"location":"reference/match/#tablite.match","title":"tablite.match","text":""},{"location":"reference/match/#tablite.match-classes","title":"Classes","text":""},{"location":"reference/match/#tablite.match-functions","title":"Functions","text":""},{"location":"reference/match/#tablite.match.match","title":"tablite.match.match(T, other, *criteria, keep_left=None, keep_right=None)","text":"

performs inner join where T matches other and removes rows that do not match.

:param: T: Table :param: other: Table :param: criteria: Each criteria must be a tuple with value comparisons in the form:

(LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\nExample:\n    ('column A', \"==\", 'column B')\n\nThis syntax follows the lookup syntax. See Lookup for details.\n

:param: keep_left: list of columns to keep. :param: keep_right: list of right columns to keep.

Source code in tablite/match.py
def match(T, other, *criteria, keep_left=None, keep_right=None):  # lookup and filter combined - drops unmatched rows.\n    \"\"\"\n    performs inner join where `T` matches `other` and removes rows that do not match.\n\n    :param: T: Table\n    :param: other: Table\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n\n        (LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\n        Example:\n            ('column A', \"==\", 'column B')\n\n        This syntax follows the lookup syntax. See Lookup for details.\n\n    :param: keep_left: list of columns to keep.\n    :param: keep_right: list of right columns to keep.\n    \"\"\"\n    assert isinstance(T, Table)\n    assert isinstance(other, Table)\n    if keep_left is None:\n        keep_left = [n for n in T.columns]\n    else:\n        type_check(keep_left, list)\n        name_check(T.columns, *keep_left)\n\n    if keep_right is None:\n        keep_right = [n for n in other.columns]\n    else:\n        type_check(keep_right, list)\n        name_check(other.columns, *keep_right)\n\n    indices = np.full(shape=(len(T),), fill_value=-1, dtype=np.int64)\n    for arg in criteria:\n        b,_,a = arg\n        if _ != \"==\":\n            raise ValueError(\"match requires A == B. For other logic visit `lookup`\")\n        if b not in T.columns:\n            raise ValueError(f\"Column {b} not found in T for criteria: {arg}\")\n        if a not in other.columns:\n            raise ValueError(f\"Column {a} not found in T for criteria: {arg}\")\n\n        index_update = find_indices(other[a][:], T[b][:], fill_value=-1)\n        indices = merge_indices(indices, index_update)\n\n    cls = type(T)\n    new = cls()\n    for name in T.columns:\n        if name in keep_left:\n            new[name] = np.compress(indices != -1, T[name][:])\n\n    for name in other.columns:\n        if name in keep_right:\n            new_name = unique_name(name, new.columns)\n            primary = np.compress(indices != -1, indices)\n            new[new_name] = np.take(other[name][:], primary)\n\n    return new\n
"},{"location":"reference/match/#tablite.match.find_indices","title":"tablite.match.find_indices(x, y, fill_value=-1)","text":"

finds index of y in x

Source code in tablite/match.py
def find_indices(x,y, fill_value=-1):  # fast.\n    \"\"\"\n    finds index of y in x\n    \"\"\"\n    # disassembly of numpy:\n    # import numpy as np\n    # x = np.array([3, 5, 7,  1,   9, 8, 6, 6])\n    # y = np.array([2, 1, 5, 10, 100, 6])\n    index = np.argsort(x)  # array([3, 0, 1, 6, 7, 2, 5, 4])\n    sorted_x = x[index]  # array([1, 3, 5, 6, 6, 7, 8, 9])\n    sorted_index = np.searchsorted(sorted_x, y)  # array([1, 0, 2, 8, 8, 3])\n    yindex = np.take(index, sorted_index, mode=\"clip\")  # array([0, 3, 1, 4, 4, 6])\n    mask = x[yindex] != y  # array([ True, False, False,  True,  True, False])\n    indices = np.ma.array(yindex, mask=mask, fill_value=fill_value)  \n    # masked_array(data=[--, 3, 1, --, --, 6], mask=[ True, False, False,  True,  True, False], fill_value=999999)\n    # --: y[0] not in x\n    # 3 : y[1] == x[3]\n    # 1 : y[2] == x[1]\n    # --: y[3] not in x\n    # --: y[4] not in x\n    # --: y[5] == x[6]\n    result = np.where(~indices.mask, indices.data, -1)  \n    return result  # array([-1,  3,  1, -1, -1,  6])\n
"},{"location":"reference/match/#tablite.match.merge_indices","title":"tablite.match.merge_indices(x1, *args, fill_value=-1)","text":"

merges x1 and x2 where

Source code in tablite/match.py
def merge_indices(x1, *args, fill_value=-1):\n    \"\"\"\n    merges x1 and x2 where \n    \"\"\"\n    # dis:\n    # >>> AA = array([-1,  3, -1, 5])\n    # >>> BB = array([-1, -1,  4, 5])\n    new = x1[:]  # = AA\n    for arg in args:\n        mask = (new == fill_value)  # array([True, False, True, False])\n        new = np.where(mask, arg, new)  # array([-1, 3, 4, 5])\n    return new   # array([-1, 3, 4, 5])\n
"},{"location":"reference/merge/","title":"Merge","text":""},{"location":"reference/merge/#tablite.merge","title":"tablite.merge","text":""},{"location":"reference/merge/#tablite.merge-classes","title":"Classes","text":""},{"location":"reference/merge/#tablite.merge-functions","title":"Functions","text":""},{"location":"reference/merge/#tablite.merge.where","title":"tablite.merge.where(T, criteria, left, right, new)","text":"

takes from LEFT where criteria is True else RIGHT. :param: T: Table :param: criteria: np.array(bool): if True take left column else take right column :param left: (str) column name :param right: (str) column name :param new: (str) new name

:returns: T

Source code in tablite/merge.py
def where(T, criteria, left, right, new):\n    \"\"\" takes from LEFT where criteria is True else RIGHT.\n    :param: T: Table\n    :param: criteria: np.array(bool): \n            if True take left column\n            else take right column\n    :param left: (str) column name\n    :param right: (str) column name\n    :param new: (str) new name\n\n    :returns: T\n    \"\"\"\n    type_check(T, Table)\n    if isinstance(criteria, np.ndarray):\n        if not criteria.dtype == \"bool\":\n            raise TypeError\n    else:\n        criteria = np.array(criteria, dtype='bool')\n\n    new_name = unique_name(new, list(T.columns))\n    T.add_column(new_name)\n    col = T[new_name]\n\n    for start,end in Config.page_steps(len(criteria)):\n        indices = np.arange(start,end)\n        left_values = T[left].get_by_indices(indices)\n        right_values = T[right].get_by_indices(indices)\n        new_values = np.where(criteria, left_values, right_values)\n        col.extend(new_values)\n\n    del T[left]\n    del T[right]\n    if new != new_name and new not in T.columns:\n        T[new] = T[new_name]\n        del T[new_name]\n    return T\n
"},{"location":"reference/mp_utils/","title":"Mp utils","text":""},{"location":"reference/mp_utils/#tablite.mp_utils","title":"tablite.mp_utils","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-attributes","title":"Attributes","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.lookup_ops","title":"tablite.mp_utils.lookup_ops = {'in': _in, 'not in': not_in, '<': operator.lt, '<=': operator.le, '>': operator.gt, '>=': operator.ge, '!=': operator.ne, '==': operator.eq} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.filter_ops","title":"tablite.mp_utils.filter_ops = {'>': operator.gt, '>=': operator.ge, '==': operator.eq, '<': operator.lt, '<=': operator.le, '!=': operator.ne, 'in': _in} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.filter_ops_from_text","title":"tablite.mp_utils.filter_ops_from_text = {'gt': '>', 'gteq': '>=', 'eq': '==', 'lt': '<', 'lteq': '<=', 'neq': '!=', 'in': _in} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-classes","title":"Classes","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-functions","title":"Functions","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.not_in","title":"tablite.mp_utils.not_in(a, b)","text":"Source code in tablite/mp_utils.py
def not_in(a, b):\n    return not operator.contains(str(a), str(b))\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.select_processing_method","title":"tablite.mp_utils.select_processing_method(fields, sp, mp)","text":"PARAMETER DESCRIPTION fields

number of fields

TYPE: int

sp

method for single processing

TYPE: callable

mp

method for multiprocessing

TYPE: callable

RETURNS DESCRIPTION _type_

description

Source code in tablite/mp_utils.py
def select_processing_method(fields, sp, mp):\n    \"\"\"\n\n    Args:\n        fields (int): number of fields\n        sp (callable): method for single processing\n        mp (callable): method for multiprocessing\n\n    Returns:\n        _type_: _description_\n    \"\"\"\n    if Config.MULTIPROCESSING_MODE == Config.FORCE:\n        m = mp\n    elif Config.MULTIPROCESSING_MODE == Config.FALSE:\n        m = sp\n    elif fields < Config.SINGLE_PROCESSING_LIMIT:\n        m = sp\n    elif max(psutil.cpu_count(logical=False), 1) < 2:\n        m = sp\n    else:\n        m = mp\n    return m\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.maskify","title":"tablite.mp_utils.maskify(arr)","text":"Source code in tablite/mp_utils.py
def maskify(arr):\n    none_mask = [False] * len(arr)  # Setting the default\n\n    for i in range(len(arr)):\n        if arr[i] is None:  # Check if our value is None\n            none_mask[i] = True\n            arr[i] = 0  # Remove None from the original array\n\n    return none_mask\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.share_mem","title":"tablite.mp_utils.share_mem(inp_arr, dtype)","text":"Source code in tablite/mp_utils.py
def share_mem(inp_arr, dtype):\n    len_ = len(inp_arr)\n    size = np.dtype(dtype).itemsize * len_\n    shape = (len_,)\n\n    out_shm = shared_memory.SharedMemory(create=True, size=size)  # the co_processors will read this.\n    out_arr_index = np.ndarray(shape, dtype=dtype, buffer=out_shm.buf)\n    out_arr_index[:] = inp_arr\n\n    return out_arr_index, out_shm\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.map_task","title":"tablite.mp_utils.map_task(data_shm_name, index_shm_name, destination_shm_name, shape, dtype, start, end)","text":"Source code in tablite/mp_utils.py
def map_task(data_shm_name, index_shm_name, destination_shm_name, shape, dtype, start, end):\n    # connect\n    shared_data = shared_memory.SharedMemory(name=data_shm_name)\n    data = np.ndarray(shape, dtype=dtype, buffer=shared_data.buf)\n\n    shared_index = shared_memory.SharedMemory(name=index_shm_name)\n    index = np.ndarray(shape, dtype=np.int64, buffer=shared_index.buf)\n\n    shared_target = shared_memory.SharedMemory(name=destination_shm_name)\n    target = np.ndarray(shape, dtype=dtype, buffer=shared_target.buf)\n    # work\n    target[start:end] = np.take(data[start:end], index[start:end])\n    # disconnect\n    shared_data.close()\n    shared_index.close()\n    shared_target.close()\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.reindex_task","title":"tablite.mp_utils.reindex_task(src, dst, index_shm, shm_shape, start, end)","text":"Source code in tablite/mp_utils.py
def reindex_task(src, dst, index_shm, shm_shape, start, end):\n    # connect\n    existing_shm = shared_memory.SharedMemory(name=index_shm)\n    shared_index = np.ndarray(shm_shape, dtype=np.int64, buffer=existing_shm.buf)\n    # work\n    array = load_numpy(src)\n    new = np.take(array, shared_index[start:end])\n    np.save(dst, new, allow_pickle=True, fix_imports=False)\n    # disconnect\n    existing_shm.close()\n
"},{"location":"reference/nimlite/","title":"Nimlite","text":""},{"location":"reference/nimlite/#tablite.nimlite","title":"tablite.nimlite","text":""},{"location":"reference/nimlite/#tablite.nimlite-attributes","title":"Attributes","text":""},{"location":"reference/nimlite/#tablite.nimlite.K","title":"tablite.nimlite.K = TypeVar('K') module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ColumnSelectorDict","title":"tablite.nimlite.ColumnSelectorDict = TypedDict('ColumnSelectorDict', {'column': str, 'type': Union[Literal['int'], Literal['float'], Literal['bool'], Literal['str'], Literal['date'], Literal['time'], Literal['datetime']], 'allow_empty': Union[bool, None], 'rename': Union[str, None]}) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.paths","title":"tablite.nimlite.paths = sys.argv[:] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite-classes","title":"Classes","text":""},{"location":"reference/nimlite/#tablite.nimlite-functions","title":"Functions","text":""},{"location":"reference/nimlite/#tablite.nimlite.text_reader_task","title":"tablite.nimlite.text_reader_task(*, pid, path, encoding, dialect, task, import_fields, guess_dtypes)","text":"Source code in tablite/nimlite.py
def text_reader_task(*, pid, path, encoding, dialect, task, import_fields, guess_dtypes):\n    return nl.text_reader_task(\n        path=path,\n        encoding=encoding,\n        dia_delimiter=dialect[\"delimiter\"],\n        dia_quotechar=dialect[\"quotechar\"],\n        dia_escapechar=dialect[\"escapechar\"],\n        dia_doublequote=dialect[\"doublequote\"],\n        dia_quoting=dialect[\"quoting\"],\n        dia_skipinitialspace=dialect[\"skipinitialspace\"],\n        dia_skiptrailingspace=dialect[\"skiptrailingspace\"],\n        dia_lineterminator=dialect[\"lineterminator\"],\n        dia_strict=dialect[\"strict\"],\n        tsk_pages=task[\"pages\"],\n        tsk_offset=task[\"offset\"],\n        tsk_count=task[\"count\"],\n        import_fields=import_fields,\n        guess_dtypes=guess_dtypes\n    )\n
"},{"location":"reference/nimlite/#tablite.nimlite.text_reader","title":"tablite.nimlite.text_reader(T: Type[Table], pid: str, path: str, encoding: Literal['ENC_UTF8'] | Literal['ENC_UTF16'] | Literal['ENC_WIN1250'] = 'ENC_UTF8', *, first_row_has_headers: bool = True, header_row_index: int = 0, columns: list[str] | None = None, start: int | None = None, limit: int | None = None, guess_datatypes: bool = False, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: str, strip_leading_and_tailing_whitespace: bool = True, tqdm=_tqdm) -> Table","text":"Source code in tablite/nimlite.py
def text_reader(\n    T,\n    pid, path,\n    encoding=\"ENC_UTF8\",\n    *,\n    first_row_has_headers=True, header_row_index=0,\n    columns=None,\n    start=None, limit=None,\n    guess_datatypes=False,\n    newline='\\n', delimiter=',', text_qualifier='\"',\n    quoting, strip_leading_and_tailing_whitespace=True,\n    tqdm=_tqdm\n):\n    assert isinstance(path, Path)\n    assert isinstance(pid, Path)\n\n    table = nl.text_reader(\n        pid=str(pid),\n        path=str(path),\n        encoding=encoding,\n        first_row_has_headers=first_row_has_headers, header_row_index=header_row_index,\n        columns=columns,\n        start=start, limit=limit,\n        guess_datatypes=guess_datatypes,\n        newline=newline, delimiter=delimiter, text_qualifier=text_qualifier,\n        quoting=quoting,\n        strip_leading_and_tailing_whitespace=strip_leading_and_tailing_whitespace,\n        page_size=Config.PAGE_SIZE\n    )\n\n    task_info = table[\"task\"]\n    task_columns = table[\"columns\"]\n\n    ti_path = task_info[\"path\"]\n    ti_encoding = task_info[\"encoding\"]\n    ti_dialect = task_info[\"dialect\"]\n    ti_guess_dtypes = task_info[\"guess_dtypes\"]\n    ti_tasks = task_info[\"tasks\"]\n    ti_import_fields = task_info[\"import_fields\"]\n    ti_import_field_names = task_info[\"import_field_names\"]\n\n    is_windows = platform.system() == \"Windows\"\n    use_logical = False if is_windows else True\n\n    cpus = max(psutil.cpu_count(logical=use_logical), 1)\n\n    tasks = [\n        Task(\n            text_reader_task,\n            path=ti_path,\n            encoding=ti_encoding,\n            dialect=ti_dialect,\n            task=t,\n            guess_dtypes=ti_guess_dtypes,\n            import_fields=ti_import_fields,\n            pid=pid\n        ) for t in ti_tasks\n    ]\n\n    is_sp = False\n\n    if Config.MULTIPROCESSING_MODE == Config.FALSE:\n        is_sp = True\n    elif Config.MULTIPROCESSING_MODE == Config.AUTO and cpus <= 1 or len(tasks) <= 1:\n        is_sp = True\n    elif Config.MULTIPROCESSING_MODE == Config.FORCE:\n        is_sp = False\n\n    if is_sp:\n        res = [\n            task.f(*task.args, **task.kwargs)\n            for task in tqdm(tasks, \"importing file\")\n        ]\n    else:\n        with TaskManager(cpus) as tm:\n            res = tm.execute(tasks, tqdm)\n\n            if not all(isinstance(r, list) for r in res):\n                raise Exception(\"failed\")\n\n    col_path = pid\n    column_dict = {\n        cols: Column(col_path)\n        for cols in ti_import_field_names\n    }\n\n    for res_pages in res:\n        col_map = {\n            n: res_pages[i]\n            for i, n in enumerate(ti_import_field_names)\n        }\n\n        for k, c in column_dict.items():\n            c.pages.append(col_map[k])\n\n    if columns is None:\n        columns = [c[\"name\"] for c in task_columns]\n\n    table_dict = {\n        a[\"name\"]: column_dict[b]\n        for a, b in zip(task_columns, columns)\n    }\n\n    table = T(columns=table_dict)\n\n    return table\n
"},{"location":"reference/nimlite/#tablite.nimlite.wrap","title":"tablite.nimlite.wrap(str_)","text":"Source code in tablite/nimlite.py
def wrap(str_):\n    return '\"' + str_.replace('\"', '\\\\\"').replace(\"'\", \"\\\\'\").replace(\"\\n\", \"\\\\n\").replace(\"\\t\", \"\\\\t\") + '\"'\n
"},{"location":"reference/nimlite/#tablite.nimlite.collect_cs_info","title":"tablite.nimlite.collect_cs_info(i: int, columns: dict, res_cols_pass: list, res_cols_fail: list)","text":"Source code in tablite/nimlite.py
def collect_cs_info(i: int, columns: dict, res_cols_pass: list, res_cols_fail: list):\n    el = {\n        k: column[i]\n        for k, column in columns.items()\n    }\n\n    col_pass = res_cols_pass[i]\n    col_fail = res_cols_fail[i]\n\n    return el, col_pass, col_fail\n
"},{"location":"reference/nimlite/#tablite.nimlite.column_select","title":"tablite.nimlite.column_select(table: K, cols: list[ColumnSelectorDict], tqdm=_tqdm, TaskManager=TaskManager) -> tuple[K, K]","text":"Source code in tablite/nimlite.py
def column_select(table, cols, tqdm=_tqdm, TaskManager=TaskManager):\n    with tqdm(total=100, desc=\"column select\", bar_format='{desc}: {percentage:3.0f}%|{bar}{r_bar}') as pbar:\n        T = type(table)\n        dir_pid = Config.workdir / Config.pid\n\n        columns, page_count, is_correct_type, desired_column_map, passed_column_data, failed_column_data, res_cols_pass, res_cols_fail, column_names, reject_reason_name = nl.collect_column_select_info(table, cols, str(dir_pid), pbar)\n\n        if all(is_correct_type.values()):\n            tbl_pass_columns = {\n                desired_name: table[desired_info[0]]\n                for desired_name, desired_info in desired_column_map.items()\n            }\n\n            tbl_fail_columns = {\n                desired_name: []\n                for desired_name in failed_column_data\n            }\n\n            tbl_pass = T(columns=tbl_pass_columns)\n            tbl_fail = T(columns=tbl_fail_columns)\n\n            return (tbl_pass, tbl_fail)\n\n        task_list_inp = (\n            collect_cs_info(i, columns, res_cols_pass, res_cols_fail)\n            for i in range(page_count)\n        )\n\n        page_size = Config.PAGE_SIZE\n\n        tasks = (\n            Task(\n                nl.do_slice_convert, str(dir_pid), page_size, columns, reject_reason_name, res_pass, res_fail, desired_column_map, column_names, is_correct_type\n            )\n            for columns, res_pass, res_fail in task_list_inp\n        )\n\n        cpu_count = max(psutil.cpu_count(), 1)\n\n        if Config.MULTIPROCESSING_MODE == Config.FORCE:\n            is_mp = True\n        elif Config.MULTIPROCESSING_MODE == Config.FALSE:\n            is_mp = False\n        elif Config.MULTIPROCESSING_MODE == Config.AUTO:\n            is_multithreaded = cpu_count > 1\n            is_multipage = page_count > 1\n\n            is_mp = is_multithreaded and is_multipage\n\n        tbl_pass = T({k: [] for k in passed_column_data})\n        tbl_fail = T({k: [] for k in failed_column_data})\n\n        converted = []\n        step_size = 45 / max(page_count - 1, 1)\n\n        class WrapUpdate:\n            def update(self, n):\n                pbar.update(n * step_size)\n\n        if is_mp:\n            with TaskManager(cpu_count=cpu_count) as tm:\n                res = tm.execute(list(tasks), pbar=WrapUpdate())\n\n                if any(isinstance(r, str) for r in res):\n                    raise Exception(\"tasks failed\")\n\n                converted.extend(res)\n        else:\n            for task in tasks:\n                res = task.execute()\n\n                if isinstance(res, str):\n                    raise Exception(res)\n\n                converted.append(res)\n                pbar.update(1)\n\n        def extend_table(table, columns):\n            for (col_name, pg) in columns:\n                table[col_name].pages.append(pg)\n\n        for pg_pass, pg_fail in converted:\n            extend_table(tbl_pass, pg_pass)\n            extend_table(tbl_fail, pg_fail)\n\n        pbar.update(pbar.total - pbar.n)\n\n        return tbl_pass, tbl_fail\n
"},{"location":"reference/nimlite/#tablite.nimlite.read_page","title":"tablite.nimlite.read_page(path: str) -> np.ndarray","text":"Source code in tablite/nimlite.py
def read_page(path):\n    return nl.read_page(path)\n
"},{"location":"reference/nimlite/#tablite.nimlite-modules","title":"Modules","text":""},{"location":"reference/pivots/","title":"Pivots","text":""},{"location":"reference/pivots/#tablite.pivots","title":"tablite.pivots","text":""},{"location":"reference/pivots/#tablite.pivots-classes","title":"Classes","text":""},{"location":"reference/pivots/#tablite.pivots-functions","title":"Functions","text":""},{"location":"reference/pivots/#tablite.pivots.pivot","title":"tablite.pivots.pivot(T, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None)","text":"

param: rows: column names to keep as rows param: columns: column names to keep as columns param: functions: aggregation functions from the Groupby class as

example:

>>> t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\n>>> t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n>>> t2.show()\n+===+===+========+=====+=====+=====+\n| # | C |function|(A=1)|(A=2)|(A=3)|\n|row|int|  str   |mixed|mixed|mixed|\n+---+---+--------+-----+-----+-----+\n|0  |  6|Sum(B)  |    2|None |None |\n|1  |  5|Sum(B)  |    4|None |None |\n|2  |  4|Sum(B)  |None |    6|None |\n|3  |  3|Sum(B)  |None |    8|None |\n|4  |  2|Sum(B)  |None |None |   10|\n|5  |  1|Sum(B)  |None |None |   12|\n+===+===+========+=====+=====+=====+\n
Source code in tablite/pivots.py
def pivot(T, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    param: rows: column names to keep as rows\n    param: columns: column names to keep as columns\n    param: functions: aggregation functions from the Groupby class as\n\n    example:\n    ```\n    >>> t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    >>> t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n    >>> t2.show()\n    +===+===+========+=====+=====+=====+\n    | # | C |function|(A=1)|(A=2)|(A=3)|\n    |row|int|  str   |mixed|mixed|mixed|\n    +---+---+--------+-----+-----+-----+\n    |0  |  6|Sum(B)  |    2|None |None |\n    |1  |  5|Sum(B)  |    4|None |None |\n    |2  |  4|Sum(B)  |None |    6|None |\n    |3  |  3|Sum(B)  |None |    8|None |\n    |4  |  2|Sum(B)  |None |None |   10|\n    |5  |  1|Sum(B)  |None |None |   12|\n    +===+===+========+=====+=====+=====+\n    ```\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if isinstance(rows, str):\n        rows = [rows]\n    if not all(isinstance(i, str) for i in rows):\n        raise TypeError(f\"Expected rows as a list of column names, not {[i for i in rows if not isinstance(i,str)]}\")\n\n    if isinstance(columns, str):\n        columns = [columns]\n    if not all(isinstance(i, str) for i in columns):\n        raise TypeError(\n            f\"Expected columns as a list of column names, not {[i for i in columns if not isinstance(i, str)]}\"\n        )\n\n    if not isinstance(values_as_rows, bool):\n        raise TypeError(f\"expected sum_on_rows as boolean, not {type(values_as_rows)}\")\n\n    keys = rows + columns\n    assert isinstance(keys, list)\n\n    extra_steps = 2\n\n    if pbar is None:\n        total = extra_steps\n\n        if len(functions) == 0:\n            total = total + len(keys)\n        else:\n            total = total + len(T)\n\n        pbar = tqdm(total=total, desc=\"pivot\")\n\n    grpby = groupby(T, keys, functions, tqdm=tqdm, pbar=pbar)\n\n    if len(grpby) == 0:  # return empty table. This must be a test?\n        pbar.update(extra_steps)\n        return Table()\n\n    # split keys to determine grid dimensions\n    row_key_index = {}\n    col_key_index = {}\n\n    r = len(rows)\n    c = len(columns)\n    g = len(functions)\n\n    records = defaultdict(dict)\n\n    for row in grpby.rows:\n        row_key = tuple(row[:r])\n        col_key = tuple(row[r : r + c])\n        func_key = tuple(row[r + c :])\n\n        if row_key not in row_key_index:\n            row_key_index[row_key] = len(row_key_index)  # Y\n\n        if col_key not in col_key_index:\n            col_key_index[col_key] = len(col_key_index)  # X\n\n        rix = row_key_index[row_key]\n        cix = col_key_index[col_key]\n        if cix in records:\n            if rix in records[cix]:\n                raise ValueError(\"this should be empty.\")\n        records[cix][rix] = func_key\n\n    pbar.update(1)\n    result = type(T)()\n\n    if values_as_rows:  # ---> leads to more rows.\n        # first create all columns left to right\n\n        n = r + 1  # rows keys + 1 col for function values.\n        cols = [[] for _ in range(n)]\n        for row, ix in row_key_index.items():\n            for col_name, f in functions:\n                cols[-1].append(f\"{f.__name__}({col_name})\")\n                for col_ix, v in enumerate(row):\n                    cols[col_ix].append(v)\n\n        for col_name, values in zip(rows + [\"function\"], cols):\n            col_name = unique_name(col_name, result.columns)\n            result[col_name] = values\n        col_length = len(cols[0])\n        cols.clear()\n\n        # then populate the sparse matrix.\n        for col_key, c in col_key_index.items():\n            col_name = \"(\" + \",\".join([f\"{col_name}={value}\" for col_name, value in zip(columns, col_key)]) + \")\"\n            col_name = unique_name(col_name, result.columns)\n            L = [None for _ in range(col_length)]\n            for r, funcs in records[c].items():\n                for ix, f in enumerate(funcs):\n                    L[g * r + ix] = f\n            result[col_name] = L\n\n    else:  # ---> leads to more columns.\n        n = r\n        cols = [[] for _ in range(n)]\n        for row in row_key_index:\n            for col_ix, v in enumerate(row):\n                cols[col_ix].append(v)  # write key columns.\n\n        for col_name, values in zip(rows, cols):\n            result[col_name] = values\n\n        col_length = len(row_key_index)\n\n        # now populate the sparse matrix.\n        for col_key, c in col_key_index.items():  # select column.\n            cols, names = [], []\n\n            for f, v in zip(functions, func_key):\n                agg_col, func = f\n                terms = \",\".join([agg_col] + [f\"{col_name}={value}\" for col_name, value in zip(columns, col_key)])\n                col_name = f\"{func.__name__}({terms})\"\n                col_name = unique_name(col_name, result.columns)\n                names.append(col_name)\n                cols.append([None for _ in range(col_length)])\n            for r, funcs in records[c].items():\n                for ix, f in enumerate(funcs):\n                    cols[ix][r] = f\n            for name, col in zip(names, cols):\n                result[name] = col\n\n    pbar.update(1)\n\n    return result\n
"},{"location":"reference/pivots/#tablite.pivots.transpose","title":"tablite.pivots.transpose(T, tqdm=_tqdm)","text":"

performs a CCW matrix rotation of the table.

Source code in tablite/pivots.py
def transpose(T, tqdm=_tqdm):\n    \"\"\"performs a CCW matrix rotation of the table.\"\"\"\n    sub_cls_check(T, Table)\n\n    if len(T.columns) == 0:\n        return type(T)()\n\n    assert isinstance(T, Table)\n    new = type(T)()\n    L = list(T.columns)\n    new[L[0]] = L[1:]\n    for row in tqdm(T.rows, desc=\"table transpose\", total=len(T)):\n        new[row[0]] = row[1:]\n    return new\n
"},{"location":"reference/pivots/#tablite.pivots.pivot_transpose","title":"tablite.pivots.pivot_transpose(T, columns, keep=None, column_name='transpose', value_name='value', tqdm=_tqdm)","text":"

Transpose a selection of columns to rows.

PARAMETER DESCRIPTION columns

column names to transpose

TYPE: list of column names

keep

column names to keep (repeat)

TYPE: list of column names DEFAULT: None

RETURNS DESCRIPTION Table

with columns transposed to rows

Example

transpose columns 1,2 and 3 and transpose the remaining columns, except sum.

Input:

| col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n|------|------|------|-----|-----|-----|-----|-----|------|\n| 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n| 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n| ...  |      |      |     |     |     |     |     |      |\n\n>>> t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\nOutput:\n|col1| col2| col3| transpose| value|\n|----|-----|-----|----------|------|\n|1234| 2345| 3456| sun      |   456|\n|1234| 2345| 3456| mon      |   567|\n|1244| 2445| 4456| mon      |     7|\n
Source code in tablite/pivots.py
def pivot_transpose(T, columns, keep=None, column_name=\"transpose\", value_name=\"value\", tqdm=_tqdm):\n    \"\"\"Transpose a selection of columns to rows.\n\n    Args:\n        columns (list of column names): column names to transpose\n        keep (list of column names): column names to keep (repeat)\n\n    Returns:\n        Table: with columns transposed to rows\n\n    Example:\n        transpose columns 1,2 and 3 and transpose the remaining columns, except `sum`.\n\n    Input:\n    ```\n    | col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n    |------|------|------|-----|-----|-----|-----|-----|------|\n    | 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n    | 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n    | ...  |      |      |     |     |     |     |     |      |\n\n    >>> t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\n    Output:\n    |col1| col2| col3| transpose| value|\n    |----|-----|-----|----------|------|\n    |1234| 2345| 3456| sun      |   456|\n    |1234| 2345| 3456| mon      |   567|\n    |1244| 2445| 4456| mon      |     7|\n    ```\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if not isinstance(columns, list):\n        raise TypeError\n\n    for i in columns:\n        if not isinstance(i, str):\n            raise TypeError\n        if i not in T.columns:\n            raise ValueError\n        if columns.count(i)>1:\n            raise ValueError(f\"Column {i} appears more than once\")\n\n    if keep is None:\n        keep = []\n    for i in keep:\n        if not isinstance(i, str):\n            raise TypeError\n        if i not in T.columns:\n            raise ValueError\n\n    if column_name in keep + columns:\n        column_name = unique_name(column_name, set_of_names=keep + columns)\n    if value_name in keep + columns + [column_name]:\n        value_name = unique_name(value_name, set_of_names=keep + columns)\n\n    new = type(T)()\n    new.add_columns(*keep + [column_name, value_name])\n    news = {name: [] for name in new.columns}\n\n    n = len(keep)\n\n    with tqdm(total=len(T), desc=\"transpose\", disable=Config.TQDM_DISABLE) as pbar:\n        for ix, row in enumerate(T[keep + columns].rows, start=1):\n            keeps = row[:n]\n            transposes = row[n:]\n\n            for name, value in zip(keep, keeps):\n                news[name].extend([value] * len(transposes))\n            for name, value in zip(columns, transposes):\n                news[column_name].append(name)\n                news[value_name].append(value)\n\n            if ix % Config.SINGLE_PROCESSING_LIMIT == 0:\n                for name, values in news.items():\n                    new[name].extend(values)\n                    values.clear()\n\n            pbar.update(1)\n\n    for name, values in news.items():\n        new[name].extend(np.array(values))\n        values.clear()\n    return new\n
"},{"location":"reference/redux/","title":"Redux","text":""},{"location":"reference/redux/#tablite.redux","title":"tablite.redux","text":""},{"location":"reference/redux/#tablite.redux-attributes","title":"Attributes","text":""},{"location":"reference/redux/#tablite.redux-classes","title":"Classes","text":""},{"location":"reference/redux/#tablite.redux-functions","title":"Functions","text":""},{"location":"reference/redux/#tablite.redux.filter_all","title":"tablite.redux.filter_all(T, **kwargs)","text":"

returns Table for rows where ALL kwargs match :param kwargs: dictionary with headers and values / boolean callable

Examples:

t = Table()\nt['a'] = [1,2,3,4]\nt['b'] = [10,20,30,40]\n\ndef f(x):\n    return x == 4\ndef g(x):\n    return x < 20\n\nt2 = t.any( **{\"a\":f, \"b\":g})\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\nt2 = t.any(a=f,b=g)\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\ndef h(x):\n    return x>=2\n\ndef i(x):\n    return x<=30\n\nt2 = t.all(a=h,b=i)\nassert [r for r in t2.rows] == [[2,20], [3, 30]]\n
Source code in tablite/redux.py
def filter_all(T, **kwargs):\n    \"\"\"\n    returns Table for rows where ALL kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n\n    Examples:\n\n        t = Table()\n        t['a'] = [1,2,3,4]\n        t['b'] = [10,20,30,40]\n\n        def f(x):\n            return x == 4\n        def g(x):\n            return x < 20\n\n        t2 = t.any( **{\"a\":f, \"b\":g})\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        t2 = t.any(a=f,b=g)\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        def h(x):\n            return x>=2\n\n        def i(x):\n            return x<=30\n\n        t2 = t.all(a=h,b=i)\n        assert [r for r in t2.rows] == [[2,20], [3, 30]]\n\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if not isinstance(kwargs, dict):\n        raise TypeError(\"did you forget to add the ** in front of your dict?\")\n    if not all([k in T.columns for k in kwargs]):\n        raise ValueError(f\"Unknown column(s): {[k for k in kwargs if k not in T.columns]}\")\n\n    mask = np.full((len(T),), True)\n    for k, v in kwargs.items():\n        col = T[k]\n        for start, end, data in col.iter_by_page():\n            if callable(v):\n                vf = np.frompyfunc(v, 1, 1)\n                mask[start:end] = mask[start:end] & np.apply_along_axis(vf, 0, data)\n            else:\n                mask[start:end] = mask[start:end] & (data == v)\n\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.drop","title":"tablite.redux.drop(T, *args)","text":"

drops all rows that contain args

PARAMETER DESCRIPTION T

TYPE: Table

Source code in tablite/redux.py
def drop(T, *args):\n    \"\"\"drops all rows that contain args\n\n    Args:\n        T (Table):\n    \"\"\"\n    sub_cls_check(T, Table)\n    mask = np.full((len(T),), False)\n    for name in T.columns:\n        col = T[name]\n        for start, end, data in col.iter_by_page():\n            for arg in args:\n                mask[start:end] = mask[start:end] | (data == arg)\n\n    mask = np.invert(mask)\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.filter_any","title":"tablite.redux.filter_any(T, **kwargs)","text":"

returns Table for rows where ANY kwargs match :param kwargs: dictionary with headers and values / boolean callable

Source code in tablite/redux.py
def filter_any(T, **kwargs):\n    \"\"\"\n    returns Table for rows where ANY kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n    \"\"\"\n    sub_cls_check(T, Table)\n    if not isinstance(kwargs, dict):\n        raise TypeError(\"did you forget to add the ** in front of your dict?\")\n\n    mask = np.full((len(T),), False)\n    for k, v in kwargs.items():\n        col = T[k]\n        for start, end, data in col.iter_by_page():\n            if callable(v):\n                vf = np.frompyfunc(v, 1, 1)\n                mask[start:end] = mask[start:end] | np.apply_along_axis(vf, 0, data)\n            else:\n                mask[start:end] = mask[start:end] | (v == data)\n\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.filter","title":"tablite.redux.filter(T, expressions, filter_type='all', tqdm=_tqdm)","text":"

filters table

PARAMETER DESCRIPTION T

Table.

TYPE: Table subclass

expressions

str: filters based on an expression, such as: \"all((A==B, C!=4, 200<D))\" which is interpreted using python's compiler to:

def _f(A,B,C,D):\n    return all((A==B, C!=4, 200<D))\n

list of dicts: (example):

L = [ {'column1':'A', 'criteria': \"==\", 'column2': 'B'}, {'column1':'C', 'criteria': \"!=\", \"value2\": '4'}, {'value1': 200, 'criteria': \"<\", column2: 'D' } ]

TYPE: list or str

accepted

'column1', 'column2', 'criteria', 'value1', 'value2'

TYPE: dictionary keys

filter_type

Ignored if expressions is str. 'all' or 'any'. Defaults to \"all\".

TYPE: str DEFAULT: 'all'

tqdm

progressbar. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION 2xTables

trues, falses

Source code in tablite/redux.py
def filter(T, expressions, filter_type=\"all\", tqdm=_tqdm):\n    \"\"\"filters table\n\n\n    Args:\n        T (Table subclass): Table.\n        expressions (list or str):\n            str:\n                filters based on an expression, such as:\n                \"all((A==B, C!=4, 200<D))\"\n                which is interpreted using python's compiler to:\n\n                def _f(A,B,C,D):\n                    return all((A==B, C!=4, 200<D))\n\n            list of dicts: (example):\n\n            L = [\n                {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n                {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n                {'value1': 200, 'criteria': \"<\", column2: 'D' }\n            ]\n\n        accepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n\n        filter_type (str, optional): Ignored if expressions is str.\n            'all' or 'any'. Defaults to \"all\".\n        tqdm (tqdm, optional): progressbar. Defaults to _tqdm.\n\n    Returns:\n        2xTables: trues, falses\n    \"\"\"\n    # determine method\n    sub_cls_check(T, Table)\n    if len(T) == 0:\n        return T.copy(), T.copy()\n\n    if isinstance(expressions, str):\n        mask = _filter_using_expression(T, expressions)\n    elif isinstance(expressions, list):\n        mask = _filter_using_list_of_dicts(T, expressions, filter_type, tqdm)\n    else:\n        raise TypeError\n    # create new tables\n    return _compress_both(T, mask)\n
"},{"location":"reference/reindex/","title":"Reindex","text":""},{"location":"reference/reindex/#tablite.reindex","title":"tablite.reindex","text":""},{"location":"reference/reindex/#tablite.reindex-classes","title":"Classes","text":""},{"location":"reference/reindex/#tablite.reindex-functions","title":"Functions","text":""},{"location":"reference/reindex/#tablite.reindex.reindex","title":"tablite.reindex.reindex(T, index, names=None, tqdm=_tqdm, pbar=None)","text":"

Constant Memory helper for reindexing pages.

Memory usage is set by datatype and Config.PAGE_SIZE

PARAMETER DESCRIPTION T

subclass of Table

TYPE: Table

index

int64.

TYPE: array

names

list of names from T to reindex.

TYPE: (list, str) DEFAULT: None

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

Defaults to None.

TYPE: pbar DEFAULT: None

RETURNS DESCRIPTION _type_

description

Source code in tablite/reindex.py
def reindex(T, index, names=None, tqdm=_tqdm, pbar=None):\n    \"\"\"Constant Memory helper for reindexing pages.\n\n    Memory usage is set by datatype and Config.PAGE_SIZE\n\n    Args:\n        T (Table): subclass of Table\n        index (np.array): int64.\n        names (list, str): list of names from T to reindex.\n        tqdm (tqdm, optional): Defaults to _tqdm.\n        pbar (pbar, optional): Defaults to None.\n\n    Returns:\n        _type_: _description_\n    \"\"\"\n    if names is None:\n        names = list(T.columns.keys())\n\n    if pbar is None:\n        total = len(names)\n        pbar = tqdm(total=total, desc=\"join\", disable=Config.TQDM_DISABLE)\n\n    sub_cls_check(T, Table)\n    cls = type(T)\n    result = cls()\n    for name in names:\n        result.add_column(name)\n        col = result[name]\n\n        for start, end in Config.page_steps(len(index)):\n            indices = index[start:end]\n            values = T[name].get_by_indices(indices)\n            # in these values, the index of -1 will be wrong.\n            # so if there is any -1 in the indices, they will\n            # have to be replaced with Nones\n            mask = indices == -1\n            if np.any(mask):\n                nones = np.full(index.shape, fill_value=None)\n                values = np.where(mask, nones, values)\n            col.extend(values)\n        pbar.update(1)\n\n    return result\n
"},{"location":"reference/sort_utils/","title":"Sort utils","text":""},{"location":"reference/sort_utils/#tablite.sort_utils","title":"tablite.sort_utils","text":""},{"location":"reference/sort_utils/#tablite.sort_utils-attributes","title":"Attributes","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.uca_collator","title":"tablite.sort_utils.uca_collator = Collator() module-attribute","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.modes","title":"tablite.sort_utils.modes = {'alphanumeric': text_sort, 'unix': unix_sort, 'excel': excel_sort} module-attribute","text":""},{"location":"reference/sort_utils/#tablite.sort_utils-functions","title":"Functions","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.text_sort","title":"tablite.sort_utils.text_sort(values, reverse=False)","text":"

Sorts everything as text.

Source code in tablite/sort_utils.py
def text_sort(values, reverse=False):\n    \"\"\"\n    Sorts everything as text.\n    \"\"\"\n    text = {str(i): i for i in values}\n    L = list(text.keys())\n    L.sort(key=uca_collator.sort_key, reverse=reverse)\n    d = {text[value]: ix for ix, value in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.unix_sort","title":"tablite.sort_utils.unix_sort(values, reverse=False)","text":"

Unix sortation sorts by the following order:

| rank | type | value | +------+-----------+--------------------------------------------+ | 0 | None | floating point -infinite | | 1 | bool | 0 as False, 1 as True | | 2 | int | as numeric value | | 2 | float | as numeric value | | 3 | time | \u03c4 * seconds into the day / (24 * 60 * 60) | | 4 | date | as integer days since 1970/1/1 | | 5 | datetime | as float using date (int) + time (decimal) | | 6 | timedelta | as float using date (int) + time (decimal) | | 7 | str | using unicode | +------+-----------+--------------------------------------------+

\u03c4 = 2 * \u03c0

Source code in tablite/sort_utils.py
def unix_sort(values, reverse=False):\n    \"\"\"\n    Unix sortation sorts by the following order:\n\n    | rank | type      | value                                      |\n    +------+-----------+--------------------------------------------+\n    |   0  | None      | floating point -infinite                   |\n    |   1  | bool      | 0 as False, 1 as True                      |\n    |   2  | int       | as numeric value                           |\n    |   2  | float     | as numeric value                           |\n    |   3  | time      | \u03c4 * seconds into the day / (24 * 60 * 60)  |\n    |   4  | date      | as integer days since 1970/1/1             |\n    |   5  | datetime  | as float using date (int) + time (decimal) |\n    |   6  | timedelta | as float using date (int) + time (decimal) |\n    |   7  | str       | using unicode                              |\n    +------+-----------+--------------------------------------------+\n\n    \u03c4 = 2 * \u03c0\n\n    \"\"\"\n    text, non_text = [], []\n\n    # L = []\n    # text = [i for i in values if isinstance(i, str)]\n    # text.sort(key=uca_collator.sort_key, reverse=reverse)\n    # text_code = _unix_typecodes[str]\n    # L = [(text_code, ix, v) for ix, v in enumerate(text)]\n\n    for value in values:\n        if isinstance(value, str):\n            text.append(value)\n        else:\n            t = type(value)\n            TC = _unix_typecodes[t]\n            tf = _unix_value_function[t]\n            VC = tf(value)\n            non_text.append((TC, VC, value))\n    non_text.sort(reverse=reverse)\n\n    text.sort(key=uca_collator.sort_key, reverse=reverse)\n    text_code = _unix_typecodes[str]\n    text = [(text_code, ix, v) for ix, v in enumerate(text)]\n\n    L = non_text + text\n    d = {value: ix for ix, (_, _, value) in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.excel_sort","title":"tablite.sort_utils.excel_sort(values, reverse=False)","text":"

Excel sortation sorts by the following order:

| rank | type | value | +------+-----------+--------------------------------------------+ | 1 | int | as numeric value | | 1 | float | as numeric value | | 1 | time | as seconds into the day / (24 * 60 * 60) | | 1 | date | as integer days since 1900/1/1 | | 1 | datetime | as float using date (int) + time (decimal) | | (1)*| timedelta | as float using date (int) + time (decimal) | | 2 | str | using unicode | | 3 | bool | 0 as False, 1 as True | | 4 | None | floating point infinite. | +------+-----------+--------------------------------------------+

  • Excel doesn't have timedelta.
Source code in tablite/sort_utils.py
def excel_sort(values, reverse=False):\n    \"\"\"\n    Excel sortation sorts by the following order:\n\n    | rank | type      | value                                      |\n    +------+-----------+--------------------------------------------+\n    |   1  | int       | as numeric value                           |\n    |   1  | float     | as numeric value                           |\n    |   1  | time      | as seconds into the day / (24 * 60 * 60)   |\n    |   1  | date      | as integer days since 1900/1/1             |\n    |   1  | datetime  | as float using date (int) + time (decimal) |\n    |  (1)*| timedelta | as float using date (int) + time (decimal) |\n    |   2  | str       | using unicode                              |\n    |   3  | bool      | 0 as False, 1 as True                      |\n    |   4  | None      | floating point infinite.                   |\n    +------+-----------+--------------------------------------------+\n\n    * Excel doesn't have timedelta.\n    \"\"\"\n\n    def tup(TC, value):\n        return (TC, _excel_value_function[t](value), value)\n\n    text, numeric, booles, nones = [], [], [], []\n    for value in values:\n        t = type(value)\n        TC = _excel_typecodes[t]\n\n        if TC == 0:\n            numeric.append(tup(TC, value))\n        elif TC == 1:\n            text.append(value)  # text is processed later.\n        elif TC == 2:\n            booles.append(tup(TC, value))\n        elif TC == 3:\n            booles.append(tup(TC, value))\n        else:\n            raise TypeError(f\"no typecode for {value}\")\n\n    if text:\n        text.sort(key=uca_collator.sort_key, reverse=reverse)\n        text = [(2, ix, v) for ix, v in enumerate(text)]\n\n    numeric.sort(reverse=reverse)\n    booles.sort(reverse=reverse)\n    nones.sort(reverse=reverse)\n\n    if reverse:\n        L = nones + booles + text + numeric\n    else:\n        L = numeric + text + booles + nones\n    d = {value: ix for ix, (_, _, value) in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.rank","title":"tablite.sort_utils.rank(values, reverse, mode)","text":"

values: list of values to sort. reverse: bool mode: as 'text', as 'numeric' or as 'excel' return: dict: d[value] = rank

Source code in tablite/sort_utils.py
def rank(values, reverse, mode):\n    \"\"\"\n    values: list of values to sort.\n    reverse: bool\n    mode: as 'text', as 'numeric' or as 'excel'\n    return: dict: d[value] = rank\n    \"\"\"\n    if mode not in modes:\n        raise ValueError(f\"{mode} not in list of modes: {list(modes)}\")\n    f = modes.get(mode)\n    return f(values, reverse)\n
"},{"location":"reference/sortation/","title":"Sortation","text":""},{"location":"reference/sortation/#tablite.sortation","title":"tablite.sortation","text":""},{"location":"reference/sortation/#tablite.sortation-attributes","title":"Attributes","text":""},{"location":"reference/sortation/#tablite.sortation-classes","title":"Classes","text":""},{"location":"reference/sortation/#tablite.sortation-functions","title":"Functions","text":""},{"location":"reference/sortation/#tablite.sortation.sort_index","title":"tablite.sortation.sort_index(T, mapping, sort_mode='excel', tqdm=_tqdm, pbar=None)","text":"

helper for methods sort and is_sorted

param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default) param: **kwargs: sort criteria. See Table.sort()

Source code in tablite/sortation.py
def sort_index(T, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar=None):\n    \"\"\"\n    helper for methods `sort` and `is_sorted`\n\n    param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default)\n    param: **kwargs: sort criteria. See Table.sort()\n    \"\"\"\n\n    sub_cls_check(T, Table)\n\n    if not isinstance(mapping, dict) or not mapping:\n        raise TypeError(\"Expected mapping (dict)?\")\n\n    for k, v in mapping.items():\n        if k not in T.columns:\n            raise ValueError(f\"no column {k}\")\n        if not isinstance(v, bool):\n            raise ValueError(f\"{k} was mapped to {v} - a non-boolean\")\n\n    if sort_mode not in sort_modes:\n        raise ValueError(f\"{sort_mode} not in list of sort_modes: {list(sort_modes)}\")\n\n    rank = {i: tuple() for i in range(len(T))}  # create index and empty tuple for sortation.\n\n    _pbar = tqdm(total=len(mapping.items()), desc=\"creating sort index\") if pbar is None else pbar\n\n    for key, reverse in mapping.items():\n        col = T[key][:]\n        ranks = sort_rank(values=[numpy_to_python(v) for v in multitype_set(col)], reverse=reverse, mode=sort_mode)\n        assert isinstance(ranks, dict)\n        for ix, v in enumerate(col):\n            v2 = numpy_to_python(v)\n            rank[ix] += (ranks[v2],)  # add tuple for each sortation level.\n\n        _pbar.update(1)\n\n    del col\n    del ranks\n\n    new_order = [(r, i) for i, r in rank.items()]  # tuples are listed and sort...\n    del rank  # free memory.\n\n    new_order.sort()\n    sorted_index = [i for _, i in new_order]  # new index is extracted.\n    new_order.clear()\n    return np.array(sorted_index, dtype=np.int64)\n
"},{"location":"reference/sortation/#tablite.sortation.reindex","title":"tablite.sortation.reindex(T, index)","text":"

index: list of integers that declare sort order.

Examples:

Table:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6]\nresult: ['b','d','f','h']\n\nTable:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6,1,3,5,7]\nresult: ['a','c','e','g','b','d','f','h']\n
Source code in tablite/sortation.py
def reindex(T, index):\n    \"\"\"\n    index: list of integers that declare sort order.\n\n    Examples:\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6]\n        result: ['b','d','f','h']\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6,1,3,5,7]\n        result: ['a','c','e','g','b','d','f','h']\n\n    \"\"\"\n    sub_cls_check(T, Table)\n    if isinstance(index, list):\n        index = np.array(index, dtype=int)\n    type_check(index, np.ndarray)\n    if max(index) >= len(T):\n        raise IndexError(\"index out of range: max(index) > len(self)\")\n    if min(index) < -len(T):\n        raise IndexError(\"index out of range: min(index) < -len(self)\")\n\n    fields = len(T) * len(T.columns)\n    m = select_processing_method(fields, _reindex, _mp_reindex)\n    return m(T, index)\n
"},{"location":"reference/sortation/#tablite.sortation.sort","title":"tablite.sortation.sort(T, mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

Perform multi-pass sorting with precedence given order of column names. sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" kwargs: keys: columns, values: 'reverse' as boolean.

examples: Table.sort('A'=False) means sort by 'A' in ascending order. Table.sort('A'=True, 'B'=False) means sort 'A' in descending order, then (2nd priority) sort B in ascending order.

Source code in tablite/sortation.py
def sort(T, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"Perform multi-pass sorting with precedence given order of column names.\n    sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\"\n    kwargs:\n        keys: columns,\n        values: 'reverse' as boolean.\n\n    examples:\n    Table.sort('A'=False) means sort by 'A' in ascending order.\n    Table.sort('A'=True, 'B'=False) means sort 'A' in descending order, then (2nd priority)\n    sort B in ascending order.\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    index = sort_index(T, mapping, sort_mode=sort_mode, tqdm=_tqdm, pbar=pbar)\n    m = select_processing_method(len(T) * len(T.columns), _sp_reindex, _mp_reindex)\n    return m(T, index, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/sortation/#tablite.sortation.is_sorted","title":"tablite.sortation.is_sorted(T, mapping, sort_mode='excel')","text":"

Performs multi-pass sorting check with precedence given order of column names.

PARAMETER DESCRIPTION mapping

sort criteria. See Table.sort()

RETURNS DESCRIPTION

bool

Source code in tablite/sortation.py
def is_sorted(T, mapping, sort_mode=\"excel\"):\n    \"\"\"Performs multi-pass sorting check with precedence given order of column names.\n\n    Args:\n        mapping: sort criteria. See Table.sort()\n        sort_mode = sort mode. See Table.sort()\n\n    Returns:\n        bool\n    \"\"\"\n    index = sort_index(T, mapping, sort_mode=sort_mode)\n    match = np.arange(len(T))\n    return np.all(index == match)\n
"},{"location":"reference/tools/","title":"Tools","text":""},{"location":"reference/tools/#tablite.tools","title":"tablite.tools","text":""},{"location":"reference/tools/#tablite.tools-attributes","title":"Attributes","text":""},{"location":"reference/tools/#tablite.tools.guess","title":"tablite.tools.guess = DataTypes.guess module-attribute","text":""},{"location":"reference/tools/#tablite.tools.xround","title":"tablite.tools.xround = DataTypes.round module-attribute","text":""},{"location":"reference/tools/#tablite.tools-classes","title":"Classes","text":""},{"location":"reference/tools/#tablite.tools-functions","title":"Functions","text":""},{"location":"reference/tools/#tablite.tools.head","title":"tablite.tools.head(path, linecount=5, delimiter=None)","text":"

Gets the head of any supported file format.

Source code in tablite/tools.py
def head(path, linecount=5, delimiter=None):\n    \"\"\"\n    Gets the head of any supported file format.\n    \"\"\"\n    return get_headers(path, linecount=linecount, delimiter=delimiter)\n
"},{"location":"reference/utils/","title":"Utils","text":""},{"location":"reference/utils/#tablite.utils","title":"tablite.utils","text":""},{"location":"reference/utils/#tablite.utils-attributes","title":"Attributes","text":""},{"location":"reference/utils/#tablite.utils.letters","title":"tablite.utils.letters = string.ascii_lowercase + string.digits module-attribute","text":""},{"location":"reference/utils/#tablite.utils.required_keys","title":"tablite.utils.required_keys = {'min', 'max', 'mean', 'median', 'stdev', 'mode', 'distinct', 'iqr_low', 'iqr_high', 'iqr', 'sum', 'summary type', 'histogram'} module-attribute","text":""},{"location":"reference/utils/#tablite.utils.summary_methods","title":"tablite.utils.summary_methods = {bool: _boolean_statistics_summary, int: _numeric_statistics_summary, float: _numeric_statistics_summary, str: _string_statistics_summary, date: _date_statistics_summary, datetime: _datetime_statistics_summary, time: _time_statistics_summary, timedelta: _timedelta_statistics_summary, type(None): _none_type_summary} module-attribute","text":""},{"location":"reference/utils/#tablite.utils-functions","title":"Functions","text":""},{"location":"reference/utils/#tablite.utils.generate_random_string","title":"tablite.utils.generate_random_string(len)","text":"Source code in tablite/utils.py
def generate_random_string(len):\n    return \"\".join(random.choice(letters) for i in range(len))\n
"},{"location":"reference/utils/#tablite.utils.type_check","title":"tablite.utils.type_check(var, kind)","text":"Source code in tablite/utils.py
def type_check(var, kind):\n    if not isinstance(var, kind):\n        raise TypeError(f\"Expected {kind}, not {type(var)}\")\n
"},{"location":"reference/utils/#tablite.utils.sub_cls_check","title":"tablite.utils.sub_cls_check(c, kind)","text":"Source code in tablite/utils.py
def sub_cls_check(c, kind):\n    if not issubclass(type(c), kind):\n        raise TypeError(f\"Expected {kind}, not {type(c)}\")\n
"},{"location":"reference/utils/#tablite.utils.name_check","title":"tablite.utils.name_check(options, *names)","text":"Source code in tablite/utils.py
def name_check(options, *names):\n    for n in names:\n        if n not in options:\n            raise ValueError(f\"{n} not in {options}\")\n
"},{"location":"reference/utils/#tablite.utils.unique_name","title":"tablite.utils.unique_name(wanted_name, set_of_names)","text":"

returns a wanted_name as wanted_name_i given a list of names which guarantees unique naming.

Source code in tablite/utils.py
def unique_name(wanted_name, set_of_names):\n    \"\"\"\n    returns a wanted_name as wanted_name_i given a list of names\n    which guarantees unique naming.\n    \"\"\"\n    if not isinstance(set_of_names, set):\n        set_of_names = set(set_of_names)\n    name, i = wanted_name, 1\n    while name in set_of_names:\n        name = f\"{wanted_name}_{i}\"\n        i += 1\n    return name\n
"},{"location":"reference/utils/#tablite.utils.expression_interpreter","title":"tablite.utils.expression_interpreter(expression, columns)","text":"

Interprets valid expressions such as:

\"all((A==B, C!=4, 200<D))\"\n
as

def _f(A,B,C,D): return all((A==B, C!=4, 200<D))

using python's compiler.

Source code in tablite/utils.py
def expression_interpreter(expression, columns):\n    \"\"\"\n    Interprets valid expressions such as:\n\n        \"all((A==B, C!=4, 200<D))\"\n\n    as:\n        def _f(A,B,C,D):\n            return all((A==B, C!=4, 200<D))\n\n    using python's compiler.\n    \"\"\"\n    if not isinstance(expression, str):\n        raise TypeError(f\"`{expression}` is not a str\")\n    if not isinstance(columns, list):\n        raise TypeError\n    if not all(isinstance(i, str) for i in columns):\n        raise TypeError\n\n    req_columns = \", \".join(i for i in columns if i in expression)\n    script = f\"def f({req_columns}):\\n    return {expression}\"\n    tree = ast.parse(script)\n    code = compile(tree, filename=\"blah\", mode=\"exec\")\n    namespace = {}\n    exec(code, namespace)\n    f = namespace[\"f\"]\n    if not callable(f):\n        raise ValueError(f\"The expression could not be parse: {expression}\")\n    return f\n
"},{"location":"reference/utils/#tablite.utils.intercept","title":"tablite.utils.intercept(A, B)","text":"

Enables calculation of the intercept of two range objects. Used to determine if a datablock contains a slice.

PARAMETER DESCRIPTION A

range

B

range

RETURNS DESCRIPTION range

The intercept of ranges A and B.

Source code in tablite/utils.py
def intercept(A, B):\n    \"\"\"Enables calculation of the intercept of two range objects.\n    Used to determine if a datablock contains a slice.\n\n    Args:\n        A: range\n        B: range\n\n    Returns:\n        range: The intercept of ranges A and B.\n    \"\"\"\n    type_check(A, range)\n    type_check(B, range)\n\n    if A.step < 1:\n        A = range(A.stop + 1, A.start + 1, 1)\n    if B.step < 1:\n        B = range(B.stop + 1, B.start + 1, 1)\n\n    if len(A) == 0:\n        return range(0)\n    if len(B) == 0:\n        return range(0)\n\n    if A.stop <= B.start:\n        return range(0)\n    if A.start >= B.stop:\n        return range(0)\n\n    if A.start <= B.start:\n        if A.stop <= B.stop:\n            start, end = B.start, A.stop\n        elif A.stop > B.stop:\n            start, end = B.start, B.stop\n        else:\n            raise ValueError(\"bad logic\")\n    elif A.start < B.stop:\n        if A.stop <= B.stop:\n            start, end = A.start, A.stop\n        elif A.stop > B.stop:\n            start, end = A.start, B.stop\n        else:\n            raise ValueError(\"bad logic\")\n    else:\n        raise ValueError(\"bad logic\")\n\n    a_steps = math.ceil((start - A.start) / A.step)\n    a_start = (a_steps * A.step) + A.start\n\n    b_steps = math.ceil((start - B.start) / B.step)\n    b_start = (b_steps * B.step) + B.start\n\n    if A.step == 1 or B.step == 1:\n        start = max(a_start, b_start)\n        step = max(A.step, B.step)\n        return range(start, end, step)\n    elif A.step == B.step:\n        a, b = min(A.start, B.start), max(A.start, B.start)\n        if (b - a) % A.step != 0:  # then the ranges are offset.\n            return range(0)\n        else:\n            return range(b, end, step)\n    else:\n        # determine common step size:\n        step = max(A.step, B.step) if math.gcd(A.step, B.step) != 1 else A.step * B.step\n        # examples:\n        # 119 <-- 17 if 1 != 1 else 119 <-- max(7, 17) if math.gcd(7, 17) != 1 else 7 * 17\n        #  30 <-- 30 if 3 != 1 else 90 <-- max(3, 30) if math.gcd(3, 30) != 1 else 3*30\n        if A.step < B.step:\n            for n in range(a_start, end, A.step):  # increment in smallest step to identify the first common value.\n                if n < b_start:\n                    continue\n                elif (n - b_start) % B.step == 0:\n                    return range(n, end, step)  # common value found.\n        else:\n            for n in range(b_start, end, B.step):\n                if n < a_start:\n                    continue\n                elif (n - a_start) % A.step == 0:\n                    return range(n, end, step)\n\n        return range(0)\n
"},{"location":"reference/utils/#tablite.utils.summary_statistics","title":"tablite.utils.summary_statistics(values, counts)","text":"

values: any type counts: integer

returns dict with: - min (int/float, length of str, date) - max (int/float, length of str, date) - mean (int/float, length of str, date) - median (int/float, length of str, date) - stdev (int/float, length of str, date) - mode (int/float, length of str, date) - distinct (number of distinct values) - iqr (int/float, length of str, date) - sum (int/float, length of str, date) - histogram (2 arrays: values, count of each values)

Source code in tablite/utils.py
def summary_statistics(values, counts):\n    \"\"\"\n    values: any type\n    counts: integer\n\n    returns dict with:\n    - min (int/float, length of str, date)\n    - max (int/float, length of str, date)\n    - mean (int/float, length of str, date)\n    - median (int/float, length of str, date)\n    - stdev (int/float, length of str, date)\n    - mode (int/float, length of str, date)\n    - distinct (number of distinct values)\n    - iqr (int/float, length of str, date)\n    - sum (int/float, length of str, date)\n    - histogram (2 arrays: values, count of each values)\n    \"\"\"\n    # determine the dominant datatype:\n    dtypes = defaultdict(int)\n    most_frequent, most_frequent_dtype = 0, int\n    for v, c in zip(values, counts):\n        dtype = type(v)\n        total = dtypes[dtype] + c\n        dtypes[dtype] = total\n        if total > most_frequent:\n            most_frequent_dtype = dtype\n            most_frequent = total\n\n    if most_frequent == 0:\n        return {}\n\n    most_frequent_dtype = max(dtypes, key=dtypes.get)\n    mask = [type(v) == most_frequent_dtype for v in values]\n    v = list(compress(values, mask))\n    c = list(compress(counts, mask))\n\n    f = summary_methods.get(most_frequent_dtype, int)\n    result = f(v, c)\n    result[\"distinct\"] = len(values)\n    result[\"summary type\"] = most_frequent_dtype.__name__\n    result[\"histogram\"] = [values, counts]\n    assert set(result.keys()) == required_keys, \"Key missing!\"\n    return result\n
"},{"location":"reference/utils/#tablite.utils.date_range","title":"tablite.utils.date_range(start, stop, step)","text":"Source code in tablite/utils.py
def date_range(start, stop, step):\n    if not isinstance(start, datetime):\n        raise TypeError(\"start is not datetime\")\n    if not isinstance(stop, datetime):\n        raise TypeError(\"stop is not datetime\")\n    if not isinstance(step, timedelta):\n        raise TypeError(\"step is not timedelta\")\n    n = (stop - start) // step\n    return [start + step * i for i in range(n)]\n
"},{"location":"reference/utils/#tablite.utils.dict_to_rows","title":"tablite.utils.dict_to_rows(d)","text":"Source code in tablite/utils.py
def dict_to_rows(d):\n    type_check(d, dict)\n    rows = []\n    max_length = max(len(i) for i in d.values())\n    order = list(d.keys())\n    rows.append(order)\n    for i in range(max_length):\n        row = [d[k][i] for k in order]\n        rows.append(row)\n    return rows\n
"},{"location":"reference/utils/#tablite.utils.calc_col_count","title":"tablite.utils.calc_col_count(letters: str)","text":"Source code in tablite/utils.py
def calc_col_count(letters: str):\n    ord_nil = ord(\"A\") - 1\n    cols_per_letter = ord(\"Z\") - ord_nil\n    col_count = 0\n\n    for i, v in enumerate(reversed(letters)):\n        col_count = col_count + (ord(v) - ord_nil) * pow(cols_per_letter, i)\n\n    return col_count\n
"},{"location":"reference/utils/#tablite.utils.calc_true_dims","title":"tablite.utils.calc_true_dims(sheet)","text":"Source code in tablite/utils.py
def calc_true_dims(sheet):\n    src = sheet._get_source()\n    last_column = None\n\n    def handleStartElement(name, attrs):\n        nonlocal last_column\n        if name == \"c\":\n            last_column = attrs\n\n    parser = expat.ParserCreate()\n    parser.buffer_text = True\n    parser.StartElementHandler = handleStartElement\n    parser.ParseFile(src)\n\n    last_index = last_column[\"r\"]\n\n    regex = re.compile(\"\\d+\")\n\n    idx, _ = next(regex.finditer(last_index)).span()\n\n    letters, digits = last_index[0:idx], int(last_index[idx:])\n\n    return calc_col_count(letters), digits\n
"},{"location":"reference/utils/#tablite.utils.fixup_worksheet","title":"tablite.utils.fixup_worksheet(worksheet)","text":"Source code in tablite/utils.py
def fixup_worksheet(worksheet):\n    try:\n        ws_cols, ws_rows = calc_true_dims(worksheet)\n\n        worksheet._max_column = ws_cols\n        worksheet._max_row = ws_rows\n    except Exception as e:\n        logging.error(f\"Failed to fetch true dimensions: {e}\")\n
"},{"location":"reference/utils/#tablite.utils.update_access_time","title":"tablite.utils.update_access_time(path)","text":"Source code in tablite/utils.py
def update_access_time(path):\n    path = Path(path)\n    stat = path.stat()\n    os.utime(path, (now(), stat.st_mtime))\n
"},{"location":"reference/utils/#tablite.utils.load_numpy","title":"tablite.utils.load_numpy(path)","text":"Source code in tablite/utils.py
def load_numpy(path):\n    update_access_time(path)\n\n    return np.load(path, allow_pickle=True, fix_imports=False)\n
"},{"location":"reference/version/","title":"Version","text":""},{"location":"reference/version/#tablite.version","title":"tablite.version","text":""},{"location":"reference/version/#tablite.version-attributes","title":"Attributes","text":""},{"location":"reference/version/#tablite.version.__version_info__","title":"tablite.version.__version_info__ = (major, minor, patch) module-attribute","text":""},{"location":"reference/version/#tablite.version.__version__","title":"tablite.version.__version__ = '.'.join(str(i) for i in __version_info__) module-attribute","text":""}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Tablite","text":""},{"location":"#contents","title":"Contents","text":"
  • introduction
  • installation
  • feature overview
  • tutorial
  • latest updates
  • credits
"},{"location":"#introduction","title":"Introduction","text":"

Tablite seeks to be the go-to library for manipulating tabular data with an api that is as close in syntax to pure python as possible.

"},{"location":"#even-smaller-memory-footprint","title":"Even smaller memory footprint","text":"

Tablite uses numpys fileformat as a backend with strong abstraction, so that copy, append & repetition of data is handled in pages. This is imperative for incremental data processing.

Tablite tests for memory footprint. One test compares the memory footprint of 10,000,000 integers where tablite will use < 1 Mb RAM in contrast to python which will require around 133.7 Mb of RAM (1M lists with 10 integers). Tablite also tests to assure that working with 1Tb of data is tolerable.

Tablite achieves this minimal memory footprint by using a temporary storage set in config.Config.workdir as tempfile.gettempdir()/tablite-tmp. If your OS (windows/linux/mac) sits on a SSD this will benefit from high IOPS and permit slices of 9,000,000,000 rows in less than a second.

"},{"location":"#multiprocessing-enabled-by-default","title":"Multiprocessing enabled by default","text":"

Tablite uses numpy whereever possible and applies multiprocessing for bypassing the GIL on all major operations. CSV import is performed in C through using nims compiler and is as fast the hardware allows.

"},{"location":"#all-algorithms-have-been-reworked-to-respect-memory-limits","title":"All algorithms have been reworked to respect memory limits","text":"

Tablite respects the limits of free memory by tagging the free memory and defining task size before each memory intensive task is initiated (join, groupby, data import, etc). If you still run out of memory you may try to reduce the config.Config.PAGE_SIZE and rerun your program.

"},{"location":"#100-support-for-all-python-datatypes","title":"100% support for all python datatypes","text":"

Tablite wants to make it easy for you to work with data. tablite.Table's behave like a dict with lists:

my_table[column name] = [... data ...].

Tablite uses datatype mapping to native numpy types where possible and uses type mapping for non-native types such as timedelta, None, date, time\u2026 e.g. what you put in, is what you get out. This is inspired by bank python.

"},{"location":"#light-weight","title":"Light weight","text":"

Tablite is ~200 kB.

"},{"location":"#helpful","title":"Helpful","text":"

Tablite wants you to be productive, so a number of helpers are available.

  • Table.import_file to import csv*, tsv, txt, xls, xlsx, xlsm, ods, zip and logs. There is automatic type detection (see tutorial.ipynb )
  • To peek into any supported file use get_headers which shows the first 10 rows.
  • Use mytable.rows and mytable.columns to iterate over rows or columns.
  • Create multi-key .index for quick lookups.
  • Perform multi-key .sort,
  • Filter using .any and .all to select specific rows.
  • use multi-key .lookup and .join to find data across tables.
  • Perform .groupby and reorganise data as a .pivot table with max, min, sum, first, last, count, unique, average, st.deviation, median and mode
  • Append / concatenate tables with += which automatically sorts out the columns - even if they're not in perfect order.
  • Should you tables be similar but not the identical you can use .stack to \"stack\" tables on top of each other

If you're still missing something add it to the wishlist

"},{"location":"#installation","title":"Installation","text":"

Get it from pypi:

Install: pip install tablite Usage: >>> from tablite import Table

"},{"location":"#build-test","title":"Build & test","text":"
install nim >= 2.0.0\nchmod +x ./build_nim.sh\nrun ./build_nim.sh\n

Should the default nim not be your desired taste, please use nims environment manager (atlas) and run source nim-2.0.0/activate.sh on UNIX or nim-2.0.0/activate.bat on windows.

install python >= 3.8\npython -m venv /your/venv/dir\nactivate /your/venv/dir\npip install -r requirements.txt\npip install -r requirements_for_testing.py\npytest ./tests\n
"},{"location":"#feature-overview","title":"Feature overview","text":"want to... this way... loop over rows [ row for row in table.rows ] loop over columns [ table[col_name] for col_name in table.columns ] slice myslice = table['A', 'B', slice(0,None,15)] get column by name my_table['A'] get row by index my_table[9_000_000_001] value update mytable['A'][2] = new value update w. list comprehension mytable['A'] = [ x*x for x in mytable['A'] if x % 2 != 0 ] join a_join = numbers.join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'], kind='left') lookup travel_plan = friends.lookup(bustable, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop')) groupby group_by = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)]) pivot table my_pivot = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False) index indices = old_table.index(*old_table.columns) sort lookup1_sorted = lookup_1.sort(**{'time': True, 'name':False, \"sort_mode\":'unix'}) filter true, false = unfiltered.filter( [{\"column1\": 'a', \"criteria\":\">=\", 'value2':3}, ... more criteria ... ], filter_type='all' ) find any any_even_rows = mytable.any('A': lambda x : x%2==0, 'B': lambda x > 0) find all all_even_rows = mytable.all('A': lambda x : x%2==0, 'B': lambda x > 0) to json json_str = my_table.to_json() from json Table.from_json(json_str)"},{"location":"#tutorial","title":"Tutorial","text":"

To learn more see the tutorial.ipynb (Jupyter notebook)

"},{"location":"#latest-updates","title":"Latest updates","text":"

See changelog.md

"},{"location":"#credits","title":"Credits","text":"
  • Martynas Kaunas - GroupBy functionality.
  • Audrius Kulikajevas - Edge case testing / various bugs, Jupyter notebook integration.
  • Sergej Sinkarenko - various bugs.
  • Ovidijus Grigas - various bugs, documentation.
  • Lori Cooper - spell checking.
"},{"location":"benchmarks/","title":"Benchmarks","text":"In\u00a0[2]: Copied!
import psutil, os, gc, shutil, tempfile\nfrom pathlib import Path\nfrom time import perf_counter, time\nfrom tablite import Table\nfrom tablite.datasets import synthetic_order_data\nfrom tablite.config import Config\n\nConfig.TQDM_DISABLE = True\n
import psutil, os, gc, shutil, tempfile from pathlib import Path from time import perf_counter, time from tablite import Table from tablite.datasets import synthetic_order_data from tablite.config import Config Config.TQDM_DISABLE = True In\u00a0[3]: Copied!
process = psutil.Process(os.getpid())\n\ndef make_tables(sizes=[1,2,5,10,20,50]):\n    # The last tables are too big for RAM (~24Gb), so I create subtables of 1M rows and append them.\n    t = synthetic_order_data(Config.PAGE_SIZE)\n    real, flat = t.nbytes()\n    print(f\"Table {len(t):,} rows is {real/1e6:,.0f} Mb on disk\")\n\n    tables = [t]  # 1M rows.\n\n    last = 1\n    t2 = t.copy()\n    for i in sizes[1:]:\n        t2 = t2.copy()\n        for _ in range(i-last):\n            t2 += synthetic_order_data(Config.PAGE_SIZE)  # these are all unique\n        last = i\n        real, flat = t2.nbytes()\n        tables.append(t2)\n        print(f\"Table {len(t2):,} rows is {real/1e6:,.0f} Mb on disk\")\n    return tables\n\ntables = make_tables()\n
process = psutil.Process(os.getpid()) def make_tables(sizes=[1,2,5,10,20,50]): # The last tables are too big for RAM (~24Gb), so I create subtables of 1M rows and append them. t = synthetic_order_data(Config.PAGE_SIZE) real, flat = t.nbytes() print(f\"Table {len(t):,} rows is {real/1e6:,.0f} Mb on disk\") tables = [t] # 1M rows. last = 1 t2 = t.copy() for i in sizes[1:]: t2 = t2.copy() for _ in range(i-last): t2 += synthetic_order_data(Config.PAGE_SIZE) # these are all unique last = i real, flat = t2.nbytes() tables.append(t2) print(f\"Table {len(t2):,} rows is {real/1e6:,.0f} Mb on disk\") return tables tables = make_tables()
Table 1,000,000 rows is 256 Mb on disk\nTable 2,000,000 rows is 512 Mb on disk\nTable 5,000,000 rows is 1,280 Mb on disk\nTable 10,000,000 rows is 2,560 Mb on disk\nTable 20,000,000 rows is 5,120 Mb on disk\nTable 50,000,000 rows is 12,800 Mb on disk\n

The values in the tables above are all unique!

In\u00a0[4]: Copied!
tables[-1]\n
tables[-1] Out[4]: ~#1234567891011 0114014953182952021-10-06T00:00:0050814119375C3-4HGQ21\u00b0XYZ1.244647268201734421.367107051830455 129320231372182021-08-26T00:00:005007718568C5-5FZU0\u00b00.55294485347516132.6980406874392537 2312569602250812021-12-21T00:00:0050197029074C2-3GTK6\u00b0XYZ1.99739754559065617.513164305723787 3414012777817432021-08-23T00:00:0050818024969C4-3BYP6\u00b0XYZ0.047497125538289577.388171617130485 459426667674262021-07-31T00:00:0050307113074C5-2CCC21\u00b0ABC1.0219215027612885.21324123446987 5612186131851272021-12-01T00:00:0050484117249C5-4WGT21\u00b00.2038764258434556712.190974436133764 676070424343982021-11-29T00:00:0050578011564C2-3LUL0\u00b0XYZ2.2367835158480444.340628097363572.......................................49,999,9939999946602693775472021-09-17T00:00:005015409706C4-3AHQ21\u00b0XYZ0.083216645843125856.56780297752790549,999,9949999955709798646952021-08-01T00:00:0050149125006C1-2FWH6\u00b01.04763923662266419.50710544462706549,999,9959999963551956078252021-07-29T00:00:0050007026992C4-3GVG21\u00b02.20440816560941411.2706443974284949,999,99699999720762240577282021-10-16T00:00:0050950113339C5-4NKS0\u00b02.1593110498135494.21575620046596149,999,9979999986577247891352021-12-21T00:00:0050069114747C2-4LYGNone1.64809640191698683.094420483625827349,999,9989999999775312438842021-12-02T00:00:0050644129345C2-5DRH6\u00b02.30911421692753110.82706867207146849,999,999100000012290713920652021-08-23T00:00:0050706119732C4-5AGB6\u00b00.488871405593691630.8580085696389939 In\u00a0[5]: Copied!
def save_load_benchmarks(tables):\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n\n    results = Table()\n    results.add_columns('rows', 'save (sec)', 'load (sec)')\n    for t in tables:\n        fn = tmp / f'{len(t)}.tpz'\n        start = perf_counter()\n        t.save(fn)\n        end = perf_counter()\n        save = round(end-start,3)\n        assert fn.exists()\n        \n        \n        start = perf_counter()\n        t2 = Table.load(fn)\n        end = perf_counter()\n        load = round(end-start,3)\n        print(f\"saving {len(t):,} rows ({fn.stat().st_size/1e6:,.0f} Mb) took {save:,.3f} seconds. loading took {load:,.3f} seconds\")\n        del t2\n        fn.unlink()\n        results.add_rows(len(t), save, load)\n    \n    r = results\n    r['save r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['save (sec)']) ]\n    r['load r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['load (sec)'])]\n\n    return results\n
def save_load_benchmarks(tables): tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) results = Table() results.add_columns('rows', 'save (sec)', 'load (sec)') for t in tables: fn = tmp / f'{len(t)}.tpz' start = perf_counter() t.save(fn) end = perf_counter() save = round(end-start,3) assert fn.exists() start = perf_counter() t2 = Table.load(fn) end = perf_counter() load = round(end-start,3) print(f\"saving {len(t):,} rows ({fn.stat().st_size/1e6:,.0f} Mb) took {save:,.3f} seconds. loading took {load:,.3f} seconds\") del t2 fn.unlink() results.add_rows(len(t), save, load) r = results r['save r/sec'] = [int(a/b) if b!=0 else \"nil\" for a,b in zip(r['rows'], r['save (sec)']) ] r['load r/sec'] = [int(a/b) if b!=0 else \"nil\" for a,b in zip(r['rows'], r['load (sec)'])] return results In\u00a0[6]: Copied!
slb = save_load_benchmarks(tables)\n
slb = save_load_benchmarks(tables)
saving 1,000,000 rows (49 Mb) took 2.148 seconds. loading took 0.922 seconds\nsaving 2,000,000 rows (98 Mb) took 4.267 seconds. loading took 1.820 seconds\nsaving 5,000,000 rows (246 Mb) took 10.618 seconds. loading took 4.482 seconds\nsaving 10,000,000 rows (492 Mb) took 21.291 seconds. loading took 8.944 seconds\nsaving 20,000,000 rows (984 Mb) took 42.603 seconds. loading took 17.821 seconds\nsaving 50,000,000 rows (2,461 Mb) took 106.644 seconds. loading took 44.600 seconds\n
In\u00a0[7]: Copied!
slb\n
slb Out[7]: #rowssave (sec)load (sec)save r/secload r/sec 010000002.1480.9224655491084598 120000004.2671.824687131098901 2500000010.6184.4824708981115573 31000000021.2918.9444696821118067 42000000042.60317.8214694501122271 550000000106.64444.64688491121076

With various compression options

In\u00a0[8]: Copied!
def save_compression_benchmarks(t):\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n\n    import zipfile  # https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile\n    methods = [(None, zipfile.ZIP_STORED, \"zip stored\"), (None, zipfile.ZIP_LZMA, \"zip lzma\")]\n    methods += [(i, zipfile.ZIP_DEFLATED, \"zip deflated\") for i in range(0,10)]\n    methods += [(i, zipfile.ZIP_BZIP2, \"zip bzip2\") for i in range(1,10)]\n\n    results = Table()\n    results.add_columns('file size (Mb)', 'method', 'write (sec)', 'read (sec)')\n    for level, method, name in methods:\n        fn = tmp / f'{len(t)}.tpz'\n        start = perf_counter()  \n        t.save(fn, compression_method=method, compression_level=level)\n        end = perf_counter()\n        write = round(end-start,3)\n        assert fn.exists()\n        size = int(fn.stat().st_size/1e6)\n        # print(f\"{name}(level={level}): {len(t):,} rows ({size} Mb) took {write:,.3f} secconds to save\", end='')\n        \n        start = perf_counter()\n        t2 = Table.load(fn)\n        end = perf_counter()\n        read = round(end-start,3)\n        # print(f\" and {end-start:,.3} seconds to load\")\n        print(\".\", end='')\n        \n        del t2\n        fn.unlink()\n        results.add_rows(size, f\"{name}(level={level})\", write, read)\n        \n    \n    r = results\n    r.sort({'write (sec)':True})\n    r['write (rps)'] = [int(1_000_000/b) for b in r['write (sec)']]\n    r['read (rps)'] = [int(1_000_000/b) for b in r['read (sec)']]\n    return results\n
def save_compression_benchmarks(t): tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) import zipfile # https://docs.python.org/3/library/zipfile.html#zipfile.ZipFile methods = [(None, zipfile.ZIP_STORED, \"zip stored\"), (None, zipfile.ZIP_LZMA, \"zip lzma\")] methods += [(i, zipfile.ZIP_DEFLATED, \"zip deflated\") for i in range(0,10)] methods += [(i, zipfile.ZIP_BZIP2, \"zip bzip2\") for i in range(1,10)] results = Table() results.add_columns('file size (Mb)', 'method', 'write (sec)', 'read (sec)') for level, method, name in methods: fn = tmp / f'{len(t)}.tpz' start = perf_counter() t.save(fn, compression_method=method, compression_level=level) end = perf_counter() write = round(end-start,3) assert fn.exists() size = int(fn.stat().st_size/1e6) # print(f\"{name}(level={level}): {len(t):,} rows ({size} Mb) took {write:,.3f} secconds to save\", end='') start = perf_counter() t2 = Table.load(fn) end = perf_counter() read = round(end-start,3) # print(f\" and {end-start:,.3} seconds to load\") print(\".\", end='') del t2 fn.unlink() results.add_rows(size, f\"{name}(level={level})\", write, read) r = results r.sort({'write (sec)':True}) r['write (rps)'] = [int(1_000_000/b) for b in r['write (sec)']] r['read (rps)'] = [int(1_000_000/b) for b in r['read (sec)']] return results In\u00a0[9]: Copied!
scb = save_compression_benchmarks(tables[0])\n
scb = save_compression_benchmarks(tables[0])
.....................
creating sort index:   0%|          | 0/1 [00:00<?, ?it/s]\rcreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 268.92it/s]\n
In\u00a0[10]: Copied!
scb[0:20]\n
scb[0:20] Out[10]: #file size (Mb)methodwrite (sec)read (sec)write (rps)read (rps) 0256zip stored(level=None)0.3960.47525252522105263 129zip lzma(level=None)95.1372.22810511448833 2256zip deflated(level=0)0.5350.59518691581680672 349zip deflated(level=1)2.150.9224651161084598 447zip deflated(level=2)2.2640.9124416961096491 543zip deflated(level=3)3.0490.833279761204819 644zip deflated(level=4)2.920.8623424651160092 742zip deflated(level=5)4.0340.8692478921150747 840zip deflated(level=6)8.5580.81168491250000 939zip deflated(level=7)13.6950.7787301912853471038zip deflated(level=8)56.9720.7921755212626261138zip deflated(level=9)122.6230.791815512642221229zip bzip2(level=1)15.1214.065661332460021329zip bzip2(level=2)16.0474.214623162373041429zip bzip2(level=3)16.8584.409593192268081529zip bzip2(level=4)17.6485.141566631945141629zip bzip2(level=5)18.6746.009535501664171729zip bzip2(level=6)19.4056.628515331508751829zip bzip2(level=7)19.9546.714501151489421929zip bzip2(level=8)20.5956.96148555143657

Conclusions

  • Fastest: zip stored with no compression takes handles
In\u00a0[11]: Copied!
def to_sql_benchmark(t, rows=1_000_000):\n    t2 = t[:rows]\n    write_start = time()\n    _ = t2.to_sql(name='1')\n    write_end = time()\n    write = round(write_end-write_start,3)\n    return ( t.to_sql.__name__, write, 0, len(t2), \"\" , \"\" )\n
def to_sql_benchmark(t, rows=1_000_000): t2 = t[:rows] write_start = time() _ = t2.to_sql(name='1') write_end = time() write = round(write_end-write_start,3) return ( t.to_sql.__name__, write, 0, len(t2), \"\" , \"\" ) In\u00a0[12]: Copied!
def to_json_benchmark(t, rows=1_000_000):\n    t2 = t[:rows]\n\n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)\n    path = tmp / \"1.json\" \n    \n    write_start = time()\n    bytestr = t2.to_json()\n    with path.open('w') as fo:\n        fo.write(bytestr)\n    write_end = time()\n    write = round(write_end-write_start,3)\n\n    read_start = time()\n    with path.open('r') as fi:\n        _ = Table.from_json(fi.read())  # <-- JSON\n    read_end = time()\n    read = round(read_end-read_start,3)\n\n    return ( t.to_json.__name__, write, read, len(t2), int(path.stat().st_size/1e6), \"\" )\n
def to_json_benchmark(t, rows=1_000_000): t2 = t[:rows] tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) path = tmp / \"1.json\" write_start = time() bytestr = t2.to_json() with path.open('w') as fo: fo.write(bytestr) write_end = time() write = round(write_end-write_start,3) read_start = time() with path.open('r') as fi: _ = Table.from_json(fi.read()) # <-- JSON read_end = time() read = round(read_end-read_start,3) return ( t.to_json.__name__, write, read, len(t2), int(path.stat().st_size/1e6), \"\" ) In\u00a0[13]: Copied!
def f(t, args):\n    rows, c1, c1_kw, c2, c2_kw = args\n    t2 = t[:rows]\n\n    call = getattr(t2, c1)\n    assert callable(call)\n\n    write_start = time()\n    call(**c1_kw)\n    write_end = time()\n    write = round(write_end-write_start,3)\n\n    for _ in range(10):\n        gc.collect()\n\n    read_start = time()\n    if callable(c2):\n        c2(**c2_kw)\n    read_end = time()\n    read = round(read_end-read_start,3)\n\n    fn = c2_kw['path']\n    assert fn.exists()\n    fs = int(fn.stat().st_size/1e6)\n    config = {k:v for k,v in c2_kw.items() if k!= 'path'}\n\n    return ( c1, write, read, len(t2), fs , str(config))\n
def f(t, args): rows, c1, c1_kw, c2, c2_kw = args t2 = t[:rows] call = getattr(t2, c1) assert callable(call) write_start = time() call(**c1_kw) write_end = time() write = round(write_end-write_start,3) for _ in range(10): gc.collect() read_start = time() if callable(c2): c2(**c2_kw) read_end = time() read = round(read_end-read_start,3) fn = c2_kw['path'] assert fn.exists() fs = int(fn.stat().st_size/1e6) config = {k:v for k,v in c2_kw.items() if k!= 'path'} return ( c1, write, read, len(t2), fs , str(config)) In\u00a0[14]: Copied!
def import_export_benchmarks(tables):\n    Config.PROCESSING_MODE = Config.FALSE\n        \n    t = sorted(tables, key=lambda x: len(x), reverse=True)[0]\n    \n    tmp = Path(tempfile.gettempdir()) / \"junk\"\n    tmp.mkdir(exist_ok=True)   \n\n    args = [\n        (   100_000, \"to_xlsx\", {'path': tmp/'1.xlsx'}, Table.from_file, {\"path\":tmp/'1.xlsx', \"sheet\":\"pyexcel_sheet1\"}),\n        (    50_000,  \"to_ods\",  {'path': tmp/'1.ods'}, Table.from_file, {\"path\":tmp/'1.ods', \"sheet\":\"pyexcel_sheet1\"} ),  # 50k rows, otherwise MemoryError.\n        ( 1_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv'}                           ),\n        ( 1_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}),\n        (10_000_000,  \"to_csv\",  {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}),\n        ( 1_000_000,  \"to_tsv\",  {'path': tmp/'1.tsv'}, Table.from_file, {\"path\":tmp/'1.tsv'}                           ),\n        ( 1_000_000, \"to_text\",  {'path': tmp/'1.txt'}, Table.from_file, {\"path\":tmp/'1.txt'}                           ),\n        ( 1_000_000, \"to_html\", {'path': tmp/'1.html'}, Table.from_file, {\"path\":tmp/'1.html'}                          ),\n        ( 1_000_000, \"to_hdf5\", {'path': tmp/'1.hdf5'}, Table.from_file, {\"path\":tmp/'1.hdf5'}                          )\n    ]\n\n    results = Table()\n    results.add_columns('method', 'write (s)', 'read (s)', 'rows', 'size (Mb)', 'config')\n\n    results.add_rows( to_sql_benchmark(t) )\n    results.add_rows( to_json_benchmark(t) )\n\n    for arg in args:\n        if len(t)<arg[0]:\n            continue\n        print(\".\", end='')\n        try:\n            results.add_rows( f(t, arg) )\n        except MemoryError:\n            results.add_rows( arg[1], \"Memory Error\", \"NIL\", args[0], \"NIL\", \"N/A\")\n    \n    r = results\n    r['read r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['read (s)']) ]\n    r['write r/sec'] = [int(a/b) if b!=0  else \"nil\" for a,b in zip(r['rows'], r['write (s)'])]\n\n    shutil.rmtree(tmp)\n    return results\n
def import_export_benchmarks(tables): Config.PROCESSING_MODE = Config.FALSE t = sorted(tables, key=lambda x: len(x), reverse=True)[0] tmp = Path(tempfile.gettempdir()) / \"junk\" tmp.mkdir(exist_ok=True) args = [ ( 100_000, \"to_xlsx\", {'path': tmp/'1.xlsx'}, Table.from_file, {\"path\":tmp/'1.xlsx', \"sheet\":\"pyexcel_sheet1\"}), ( 50_000, \"to_ods\", {'path': tmp/'1.ods'}, Table.from_file, {\"path\":tmp/'1.ods', \"sheet\":\"pyexcel_sheet1\"} ), # 50k rows, otherwise MemoryError. ( 1_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv'} ), ( 1_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}), (10_000_000, \"to_csv\", {'path': tmp/'1.csv'}, Table.from_file, {\"path\":tmp/'1.csv', \"guess_datatypes\":False}), ( 1_000_000, \"to_tsv\", {'path': tmp/'1.tsv'}, Table.from_file, {\"path\":tmp/'1.tsv'} ), ( 1_000_000, \"to_text\", {'path': tmp/'1.txt'}, Table.from_file, {\"path\":tmp/'1.txt'} ), ( 1_000_000, \"to_html\", {'path': tmp/'1.html'}, Table.from_file, {\"path\":tmp/'1.html'} ), ( 1_000_000, \"to_hdf5\", {'path': tmp/'1.hdf5'}, Table.from_file, {\"path\":tmp/'1.hdf5'} ) ] results = Table() results.add_columns('method', 'write (s)', 'read (s)', 'rows', 'size (Mb)', 'config') results.add_rows( to_sql_benchmark(t) ) results.add_rows( to_json_benchmark(t) ) for arg in args: if len(t) In\u00a0[15]: Copied!
ieb = import_export_benchmarks(tables)\n
ieb = import_export_benchmarks(tables)
.........writing 12,000,000 records to /tmp/junk/1.hdf5... done\n
In\u00a0[16]: Copied!
ieb\n
ieb Out[16]: #methodwrite (s)read (s)rowssize (Mb)configread r/secwrite r/sec 0to_sql12.34501000000nil81004 1to_json10.8144.406100000014222696392472 2to_xlsx10.56921.5721000009{'sheet': 'pyexcel_sheet1'}46359461 3to_ods29.17529.487500003{'sheet': 'pyexcel_sheet1'}16951713 4to_csv14.31515.7311000000108{}6356869856 5to_csv14.4388.1691000000108{'guess_datatypes': False}12241469261 6to_csv140.64599.45100000001080{'guess_datatypes': False}10055371100 7to_tsv13.83415.7631000000108{}6343972285 8to_text13.93715.6821000000108{}6376771751 9to_html12.5780.531000000228{}18867927950310to_hdf55.0112.3451000000316{}81004199600

Conclusions

Best:

  • to/from JSON wins with 2.3M rps read
  • to/from CSV/TSV/TEXT comes 2nd with config guess_datatypes=False with ~ 100k rps

Worst:

  • to/from ods burst the memory footprint and hence had to be reduced to 100k rows. It also had the slowest read rate with 1450 rps.
In\u00a0[17]: Copied!
def contains_benchmark(table):\n    results = Table()\n    results.add_columns( \"column\", \"time (s)\" )\n    for name,col in table.columns.items():\n        n = len(col)\n        start,stop,step = int(n*0.02), int(n*0.98), int(n/100)\n        selection = col[start:stop:step]\n        total_time = 0.0\n        for v in selection:\n            start_time = perf_counter()\n            v in col  # <--- test!\n            end_time = perf_counter()\n            total_time += (end_time - start_time)\n        avg_time = total_time / len(selection)\n        results.add_rows( name, round(avg_time,3) )\n\n    return results\n
def contains_benchmark(table): results = Table() results.add_columns( \"column\", \"time (s)\" ) for name,col in table.columns.items(): n = len(col) start,stop,step = int(n*0.02), int(n*0.98), int(n/100) selection = col[start:stop:step] total_time = 0.0 for v in selection: start_time = perf_counter() v in col # <--- test! end_time = perf_counter() total_time += (end_time - start_time) avg_time = total_time / len(selection) results.add_rows( name, round(avg_time,3) ) return results In\u00a0[18]: Copied!
has_it = contains_benchmark(tables[-1])\nhas_it\n
has_it = contains_benchmark(tables[-1]) has_it Out[18]: #columntime (s) 0#0.001 110.043 220.032 330.001 440.001 550.001 660.006 770.003 880.006 990.00710100.04311110.655 In\u00a0[19]: Copied!
def slicing_benchmark(table):\n    n = len(table)\n    start,stop,step = int(0.02*n), int(0.98*n), int(n / 20)  # from 2% to 98% in 20 large steps\n    start_time = perf_counter()\n    snip = table[start:stop:step]\n    end_time = perf_counter()\n    print(f\"reading {len(table):,} rows to find {len(snip):,} rows took {end_time-start_time:.3f} sec\")\n    return snip\n
def slicing_benchmark(table): n = len(table) start,stop,step = int(0.02*n), int(0.98*n), int(n / 20) # from 2% to 98% in 20 large steps start_time = perf_counter() snip = table[start:stop:step] end_time = perf_counter() print(f\"reading {len(table):,} rows to find {len(snip):,} rows took {end_time-start_time:.3f} sec\") return snip In\u00a0[20]: Copied!
slice_it = slicing_benchmark(tables[-1])\n
slice_it = slicing_benchmark(tables[-1])
reading 50,000,000 rows to find 20 rows took 1.435 sec\n
In\u00a0[22]: Copied!
def column_selection_benchmark(tables):\n    results = Table()\n    results.add_columns( 'rows')\n    results.add_columns(*[f\"n cols={i}\" for i,_ in enumerate(tables[0].columns,start=1)])\n\n    for table in tables:\n        rr = [len(table)]\n        for ix, name in enumerate(table.columns):\n            cols = list(table.columns)[:ix+1]\n            start_time = perf_counter()\n            table[cols]\n            end_time = perf_counter()\n            rr.append(f\"{end_time-start_time:.5f}\")\n        results.add_rows( rr )\n    return results\n
def column_selection_benchmark(tables): results = Table() results.add_columns( 'rows') results.add_columns(*[f\"n cols={i}\" for i,_ in enumerate(tables[0].columns,start=1)]) for table in tables: rr = [len(table)] for ix, name in enumerate(table.columns): cols = list(table.columns)[:ix+1] start_time = perf_counter() table[cols] end_time = perf_counter() rr.append(f\"{end_time-start_time:.5f}\") results.add_rows( rr ) return results In\u00a0[23]: Copied!
csb = column_selection_benchmark(tables)\nprint(\"times below are are in seconds\")\ncsb\n
csb = column_selection_benchmark(tables) print(\"times below are are in seconds\") csb
times below are are in seconds\n
Out[23]: #rowsn cols=1n cols=2n cols=3n cols=4n cols=5n cols=6n cols=7n cols=8n cols=9n cols=10n cols=11n cols=12 010000000.000010.000060.000040.000040.000040.000040.000040.000040.000040.000040.000040.00004 120000000.000010.000080.000030.000030.000030.000030.000030.000030.000030.000030.000040.00004 250000000.000010.000050.000040.000040.000040.000040.000040.000040.000040.000040.000040.00004 3100000000.000020.000050.000040.000040.000040.000040.000070.000050.000050.000050.000050.00005 4200000000.000030.000060.000050.000050.000050.000050.000060.000060.000060.000060.000060.00006 5500000000.000090.000110.000100.000090.000090.000090.000090.000090.000090.000090.000100.00009 In\u00a0[33]: Copied!
def iterrows_benchmark(table):\n    results = Table()\n    results.add_columns( 'n columns', 'time (s)')\n\n    columns = ['1']\n    for column in list(table.columns):\n        columns.append(column)\n        snip = table[columns, slice(500_000,1_500_000)]\n        start_time = perf_counter()\n        counts = 0\n        for row in snip.rows:\n            counts += 1\n        end_time = perf_counter()\n        results.add_rows( len(columns), round(end_time-start_time,3))\n\n    return results\n
def iterrows_benchmark(table): results = Table() results.add_columns( 'n columns', 'time (s)') columns = ['1'] for column in list(table.columns): columns.append(column) snip = table[columns, slice(500_000,1_500_000)] start_time = perf_counter() counts = 0 for row in snip.rows: counts += 1 end_time = perf_counter() results.add_rows( len(columns), round(end_time-start_time,3)) return results In\u00a0[34]: Copied!
iterb = iterrows_benchmark(tables[-1])\niterb\n
iterb = iterrows_benchmark(tables[-1]) iterb Out[34]: #n columnstime (s) 029.951 139.816 249.859 359.93 469.985 579.942 689.958 799.867 8109.96 9119.93210129.8311139.861 In\u00a0[35]: Copied!
import matplotlib.pyplot as plt\nplt.plot(iterb['n columns'], iterb['time (s)'])\nplt.show()\n
import matplotlib.pyplot as plt plt.plot(iterb['n columns'], iterb['time (s)']) plt.show() In\u00a0[28]: Copied!
tables[-1].types()\n
tables[-1].types() Out[28]:
{'#': {int: 50000000},\n '1': {int: 50000000},\n '2': {str: 50000000},\n '3': {int: 50000000},\n '4': {int: 50000000},\n '5': {int: 50000000},\n '6': {str: 50000000},\n '7': {str: 50000000},\n '8': {str: 50000000},\n '9': {str: 50000000},\n '10': {float: 50000000},\n '11': {str: 50000000}}
In\u00a0[29]: Copied!
def dtypes_benchmark(tables):\n    dtypes_results = Table()\n    dtypes_results.add_columns(\"rows\", \"time (s)\")\n\n    for table in tables:\n        start_time = perf_counter()\n        dt = table.types()\n        end_time = perf_counter()\n        assert isinstance(dt, dict) and len(dt) != 0\n        dtypes_results.add_rows( len(table), round(end_time-start_time, 3) )\n\n    return dtypes_results\n
def dtypes_benchmark(tables): dtypes_results = Table() dtypes_results.add_columns(\"rows\", \"time (s)\") for table in tables: start_time = perf_counter() dt = table.types() end_time = perf_counter() assert isinstance(dt, dict) and len(dt) != 0 dtypes_results.add_rows( len(table), round(end_time-start_time, 3) ) return dtypes_results In\u00a0[30]: Copied!
dtype_b = dtypes_benchmark(tables)\ndtype_b\n
dtype_b = dtypes_benchmark(tables) dtype_b Out[30]: #rowstime (s) 010000000.0 120000000.0 250000000.0 3100000000.0 4200000000.0 5500000000.001 In\u00a0[31]: Copied!
def any_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n\n    for table in tables:\n        tmp = [len(table)]\n        for column in list(table.columns):\n            v = table[column][0]\n            start_time = perf_counter()\n            _ = table.any(**{column: v})\n            end_time = perf_counter()           \n            tmp.append(round(end_time-start_time,3))\n\n        results.add_rows( tmp )\n    return results\n
def any_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): v = table[column][0] start_time = perf_counter() _ = table.any(**{column: v}) end_time = perf_counter() tmp.append(round(end_time-start_time,3)) results.add_rows( tmp ) return results In\u00a0[32]: Copied!
anyb = any_benchmark(tables)\nanyb\n
anyb = any_benchmark(tables) anyb Out[32]: ~rows#1234567891011 010000000.1330.1330.1780.1330.2920.1470.1690.1430.2270.2590.1460.17 120000000.2680.2630.3430.2650.5670.2940.3350.2750.4640.5230.2890.323 250000000.6690.6530.9140.6691.4360.7230.8380.6941.1741.3350.6780.818 3100000001.3141.351.7451.3362.9021.491.6831.4142.3542.6181.3431.536 4200000002.5562.5343.3372.6025.6452.8273.2252.6464.5145.082.6933.083 5500000006.5716.4238.4556.69914.4847.9897.7986.25910.98912.486.7327.767 In\u00a0[36]: Copied!
def all_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n\n    for table in tables:\n        tmp = [len(table)]\n        for column in list(table.columns):\n            v = table[column][0]\n            start_time = perf_counter()\n            _ = table.all(**{column: v})\n            end_time = perf_counter()           \n            tmp.append(round(end_time-start_time,3))\n\n        results.add_rows( tmp )\n    return results\n
def all_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): v = table[column][0] start_time = perf_counter() _ = table.all(**{column: v}) end_time = perf_counter() tmp.append(round(end_time-start_time,3)) results.add_rows( tmp ) return results In\u00a0[37]: Copied!
allb = all_benchmark(tables)\nallb\n
allb = all_benchmark(tables) allb Out[37]: ~rows#1234567891011 010000000.120.1210.1620.1220.2640.1380.1550.1270.2090.2370.1330.151 120000000.2370.2350.3110.2380.520.2660.2970.3410.4510.530.2610.285 250000000.6750.6980.9520.5941.6050.6590.8120.7191.2241.3530.6640.914 3100000001.3141.3321.7071.3323.0911.4631.7811.3662.3582.6381.4091.714 4200000002.5762.3133.112.3965.2072.5732.9212.4034.0414.6582.4632.808 5500000005.8965.827.735.95612.9097.457.275.98110.18311.5766.3727.414 In\u00a0[\u00a0]: Copied!
\n
In\u00a0[38]: Copied!
def unique_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n        length = len(table)\n\n        tmp = [len(table)]\n        for column in list(table.columns):\n            start_time = perf_counter()\n            try:\n                L = table[column].unique()\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            assert 0 < len(L) <= length    \n\n        results.add_rows( tmp )\n    return results\n
def unique_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: length = len(table) tmp = [len(table)] for column in list(table.columns): start_time = perf_counter() try: L = table[column].unique() dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) assert 0 < len(L) <= length results.add_rows( tmp ) return results In\u00a0[39]: Copied!
ubm = unique_benchmark(tables)\nubm\n
ubm = unique_benchmark(tables) ubm Out[39]: ~rows#1234567891011 010000000.0220.0810.2480.0440.0160.0610.1150.1360.0960.0850.0940.447 120000000.1760.2710.5050.0870.0310.1240.2290.2790.1980.170.3051.471 250000000.1980.4991.2630.2180.0760.3110.570.6850.4740.4250.5952.744 3100000000.5021.1232.5350.4330.1550.6151.1281.3750.960.851.3165.826 4200000000.9562.3365.0350.8830.3191.2292.2682.7481.9131.7462.73311.883 5500000002.3956.01912.4992.1780.7643.0735.6086.8194.8284.2797.09730.511 In\u00a0[40]: Copied!
def index_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n\n        tmp = [len(table)]\n        for column in list(table.columns):\n            start_time = perf_counter()\n            try:\n                _ = table.index(column)\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            \n        results.add_rows( tmp )\n    return results\n
def index_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: tmp = [len(table)] for column in list(table.columns): start_time = perf_counter() try: _ = table.index(column) dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) results.add_rows( tmp ) return results In\u00a0[41]: Copied!
ibm = index_benchmark(tables)\nibm\n
ibm = index_benchmark(tables) ibm Out[41]: ~rows#1234567891011 010000001.9491.7931.4321.1061.0511.231.3381.4931.4111.3031.9992.325 120000002.8833.5172.8562.2172.1242.4622.6762.9862.7092.6064.0494.461 250000006.3829.0497.0965.6285.3536.3126.6497.5216.716.45910.2710.747 31000000012.55318.50613.9511.33510.72412.50913.3315.05113.50212.89919.76921.999 42000000024.71737.89628.56822.66621.47226.32727.15730.06427.33225.82238.31143.399 55000000063.01697.07772.00755.60954.09961.79768.23675.0769.02266.15299.183109.969

Multi-column index next:

In\u00a0[42]: Copied!
def multi_column_index_benchmark(tables):\n    \n    selection = [\"4\", \"7\", \"8\", \"9\"]\n    results = Table()\n    results.add_columns(\"rows\", *range(1,len(selection)+1))\n    \n    for table in tables:\n\n        tmp = [len(table)]\n        for index in range(1,5):\n            start_time = perf_counter()\n            try:\n                _ = table.index(*selection[:index])\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            tmp.append(round(dt,3))\n            print('.', end='')\n            \n        results.add_rows( tmp )\n    return results\n
def multi_column_index_benchmark(tables): selection = [\"4\", \"7\", \"8\", \"9\"] results = Table() results.add_columns(\"rows\", *range(1,len(selection)+1)) for table in tables: tmp = [len(table)] for index in range(1,5): start_time = perf_counter() try: _ = table.index(*selection[:index]) dt = perf_counter() - start_time except MemoryError: dt = -1 tmp.append(round(dt,3)) print('.', end='') results.add_rows( tmp ) return results In\u00a0[43]: Copied!
mcib = multi_column_index_benchmark(tables)\nmcib\n
mcib = multi_column_index_benchmark(tables) mcib
........................
Out[43]: #rows1234 010000001.0582.1333.2154.052 120000002.124.2786.5468.328 250000005.30310.8916.69320.793 31000000010.58122.40733.46241.91 42000000021.06445.95467.78184.828 55000000052.347109.551166.6211.053 In\u00a0[44]: Copied!
def drop_duplicates_benchmark(tables):\n    results = Table()\n    results.add_columns(\"rows\", *list(tables[0].columns))\n    \n    for table in tables:\n        result = [len(table)]\n        cols = []\n        for name in list(table.columns):\n            cols.append(name)\n            start_time = perf_counter()\n            try:\n                _ = table.drop_duplicates(*cols)\n                dt = perf_counter() - start_time\n            except MemoryError:\n                dt = -1\n            result.append(round(dt,3))\n            print('.', end='')\n        \n        results.add_rows( result )\n    return results\n
def drop_duplicates_benchmark(tables): results = Table() results.add_columns(\"rows\", *list(tables[0].columns)) for table in tables: result = [len(table)] cols = [] for name in list(table.columns): cols.append(name) start_time = perf_counter() try: _ = table.drop_duplicates(*cols) dt = perf_counter() - start_time except MemoryError: dt = -1 result.append(round(dt,3)) print('.', end='') results.add_rows( result ) return results In\u00a0[45]: Copied!
ddb = drop_duplicates_benchmark(tables)\nddb\n
ddb = drop_duplicates_benchmark(tables) ddb
........................................................................
Out[45]: ~rows#1234567891011 010000001.7612.3583.3133.9014.6154.9615.8356.5347.4548.1088.8039.682 120000003.0114.936.9347.979.26410.26812.00613.51714.9216.63117.93219.493 250000006.82713.85318.63721.23724.54827.1131.15735.02638.99243.53146.02250.433 31000000013.23831.74641.14146.91753.17258.24167.99274.65182.7491.45897.666104.82 42000000025.93277.75100.34109.314123.514131.874148.432163.57179.121196.047208.686228.059 55000000064.237312.222364.886388.249429.724466.685494.418535.367581.666607.306634.343683.858"},{"location":"benchmarks/#benchmarks","title":"Benchmarks\u00b6","text":"

These benchmarks seek to establish the performance of tablite as a user sees it.

Overview

Input/Output Various column functions Base functions Core functions - Save / Load .tpz format- Save tables to various formats- Import data from various formats - Setitem / getitem- iter- equal, not equal- copy- t += t- t *= t- contains- remove all- replace- index- unique- histogram- statistics- count - Setitem / getitem- iter / rows- equal, not equal- load- save- copy- stack- types- display_dict- show- to_dict- as_json_serializable- index - expression- filter- sort_index- reindex- drop_duplicates- sort- is_sorted- any- all- drop - replace- groupby- pivot- joins- lookup- replace missing values- transpose- pivot_transpose- diff"},{"location":"benchmarks/#input-output","title":"Input / Output\u00b6","text":""},{"location":"benchmarks/#create-tables-from-synthetic-data","title":"Create tables from synthetic data.\u00b6","text":""},{"location":"benchmarks/#save-load-tpz-format","title":"Save / Load .tpz format\u00b6","text":"

Without default compression settings (10% slower than uncompressed, 20% of uncompressed filesize)

"},{"location":"benchmarks/#save-load-tables-to-from-various-formats","title":"Save / load tables to / from various formats\u00b6","text":"

The handlers for saving / export are:

  • to_sql
  • to_json
  • to_xls
  • to_ods
  • to_csv
  • to_tsv
  • to_text
  • to_html
  • to_hdf5
"},{"location":"benchmarks/#various-column-functions","title":"Various column functions\u00b6","text":"
  • Setitem / getitem
  • iter
  • equal, not equal
  • copy
  • t += t
  • t *= t
  • contains
  • remove all
  • replace
  • index
  • unique
  • histogram
  • statistics
  • count
"},{"location":"benchmarks/#various-table-functions","title":"Various table functions\u00b6","text":""},{"location":"benchmarks/#slicing","title":"Slicing\u00b6","text":"

Slicing operations are used in many places.

"},{"location":"benchmarks/#tabletypes","title":"Table.types()\u00b6","text":"

Table.types() is implemented for near constant speed lookup.

Here is an example:

"},{"location":"benchmarks/#tableany","title":"Table.any\u00b6","text":""},{"location":"benchmarks/#tableall","title":"Table.all\u00b6","text":""},{"location":"benchmarks/#tablefilter","title":"Table.filter\u00b6","text":""},{"location":"benchmarks/#tableunique","title":"Table.unique\u00b6","text":""},{"location":"benchmarks/#tableindex","title":"Table.index\u00b6","text":"

Single column index first:

"},{"location":"benchmarks/#drop-duplicates","title":"drop duplicates\u00b6","text":""},{"location":"changelog/","title":"Changelog","text":"Version Change 2023.9.0 Adding Table.match operation. 2023.8.0 Nim backend for csv importer.Improve excel importer.Improve slicing consistency.Logical cores re-enabled on *nix based systems.Filter is now type safe.Added merge utility.Various bugfixes. 2023.6.5 Fix issues with get_headers falling back to text reading when reading 0 lines of excel, fix issue where reading excel file would ignore file count, excel file reader now has parity for linecount selection. 2023.6.4 Fix a logic bug in get_headers that caused one extra line to be returned than requested. 2023.6.3 Updated the way reference counting works. Tablite now tracks references to used pages and cleans them up based on number of references to those pages in the current process. This change allows to handle deep table clones when sending tables via processes (pickling/unpickling), whereas previous implementation would corrupt all tables using same pages due to reference counting asserting that all tables are shallow copies to the same object. 2023.6.2 Updated mplite dependency, changed to soft version requirement to prevent pipeline freezes due to small bugfixes in mplite. 2023.6.1 Major change of the backend processes. Speed up of ~6x. For more see the release notes 2022.11.19 Fixed some memory leaks. 2022.11.18 copy, filter, sort, any, all methods now properly respects the table subclass.Filter for tables with under SINGLE_PROCESSING_LIMIT rows will run on same process to reduce overhead.Errors within child processes now properly propagate to parent.Table.reset_storage(include_imports=True) now allows the user to reset the storage but exclude any imported files by setting include_imports=False during Table.reset(...).Bug: A column with 1,None,2 would be written to csv & tsv as \"1,None,2\". Now it is written \"1,,2\" where None means absent.Fix mp join producing mismatched columns lengths when different table lengths are used as an input or when join product is longer than the input table. 2022.11.17 Table.load now properly subclassess the table instead of always resulting in tablite.Table.Table.from_* methods now respect subclassess, fixed some from_* methods which were instance methods and not class methods.Fixed Table.from_dict only accepting list and tuple but not tablite.Column which is an equally valid type.Fix lookup parity in single process and multiple process outputs.Fix an issue with multiprocess lookup where no matches would throw instead of producing None.Fix an issue with filtering an empty table. 2022.11.16 Changed join to process 1M rows per task to avoid potential OOM on lower memory systems. Added mp_merge_columns to MemoryManager that merges column pages into a single column.Fix join parity in single process and multiple process outputs.Fix an issue with multiprocess join where no matches would throw instead of producing None. 2022.11.15 Bump mplite to avoid deadlock issues OS kill the process. 2022.11.14 Improve locking mechanism to allow retries when opening file as the previous solution could cause deadlocks when running multiple threads. 2022.11.13 Fix an issue with copying empty pages. 2022.11.12 Tablite now is now able to create it's own temporary directory. 2022.11.11 text_reader tqdm tracks the entire process now. text_reader properly respects free memory in *nix based systems. text_reader no longer discriminates against hyperthreaded cores. 2022.11.10 get_headers now uses plain openpyxl instead of pyexcel wrapper to speed up fetch times ~10x on certain files. 2022.11.9 get_headers can fail safe on unrecognized characters. 2022.11.8 Fix a bug with task size calculation on single core systems. 2022.11.7 Added TABLITE_TMPDIR environment variable for setting tablite work directory. Characters that fail to be read text reader due to improper encoding will be skipped. Fixed an issue where single column text files with no column delimiters would be imported as empty tables. 2022.11.6 Date inference fix 2022.11.5 Fixed negative slicing issues 2022.11.4 Transpose API changes: table.transpose(...) was renamed to table.pivot_transpose(...) new table.transpose() and table.T were added, it's functionality acts similarly to numpy.T, the column headers are used the first row in the table when transposing. 2022.11.3 Bugfix for non-ascii encoded strings during t.add_rows(...) 2022.11.2 As utf-8 is ascii compatible, the file reader utils selects utf-8 instead of ascii as a default. 2022.11.1 bugfix in datatypes.infer() where 1 was inferred as int, not float. 2022.11.0 New table features: Table.diff(other, columns=...), table.remove_duplicates_rows(), table.drop_na(*arg),table.replace(target,replacement), table.imputation(sources, targets, methods=...), table.to_pandas() and Table.from_pandas(pd.DataFrame),table.to_dict(columns, slice), Table.from_dict(),table.transpose(columns, keep, ...), New column features: Column.count(item), Column[:] is guaranteed to return a python list.Column.to_numpy(slice) returns np.ndarray. new tools library: from tablite import tools with: date_range(start,end), xround(value, multiple, up=None), and, guess as short-cut for Datatypes.guess(...). bugfixes: __eq__ was updated but missed __ne__.in operator in filter would crash if datatypes were not strings. 2022.10.11 filter now accepts any expression (str) that can be compiled by pythons compiler 2022.10.11 Bugfix for .any and .all. The code now executes much faster 2022.10.10 Bugfix for Table.import_file: import_as has been removed from keywords. 2022.10.10 All Table functions now have tqdm progressbar. 2022.10.10 More robust calculation for task size for multiprocessing. 2022.10.10 Dependency update: mplite==1.2.0 is now required. 2022.10.9 Bugfix for Table.import_file: files with duplicate header names would only have last duplicate name imported.Now the headers are made unique using name_x where x is a number. 2022.10.8 Bugfix for groupby: Where keys are empty error should have been raised.Where there are no functions, unique keypairs are returned. 2022.10.7 Bugfix for Column.statistics() for an empty column 2022.10.6 Bugfix for __setitem__: tbl['a'] = [] is now seen as tbl.add_column('a')Bugfix for __getitem__: calling a missing key raises keyerror. 2022.10.5 Bugfix for summary statistics. 2022.10.4 Bugfix for join shortcut. 2022.10.3 Bugfix for DataTypes where bool was evaluated wrongly 2022.10.0 Added ability to reindex in table.reindex(index=[0,1...,n,n-1]) 2022.9.0 Added ability to store python objects (example).Added warning when user iterates over non-rectangular dataset. 2022.8.0 Added table.export(path) which exports tablite Tables to file format given by the file extension. For example my_table.export('example.xlsx').supported formats are: json, html, xlsx, xls, csv, tsv, txt, ods and sql. 2022.7.8 Added ability to forward tqdm progressbar into Table.import_file(..., tqdm=your_tqdm), so that Jupyter notebook can use it in display-methods. 2022.7.7 Added method Table.to_sql() for export to ANSI-92 SQL enginesBugfix on to_json for timedelta. Jupyter notebook provides nice view using Table._repr_html_() JS-users can use .as_json_serializable where suitable. 2022.7.6 get_headers now takes argument (path, linecount=10) 2022.7.5 added helper Table.as_json_serializable as Jupyterkernel compat. 2022.7.4 adder helper Table.to_dict, and updated Table.to_json 2022.7.3 table.to_json now takes kwargs: row_count, columns, slice_, start_on 2022.7.2 documentation update. 2022.7.1 minor bugfix. 2022.7.0 BREAKING CHANGES- Tablite now uses HDF5 as backend. - Has multiprocessing enabled by default. - Is 20x faster. - Completely new API. 2022.6.0 DataTypes.guess([list of strings]) returns the best matching python datatype."},{"location":"tutorial/","title":"Tutorial","text":"In\u00a0[1]: Copied!
from tablite import Table\n\n## To create a tablite table is as simple as populating a dictionary:\nt = Table({'A':[1,2,3], 'B':['a','b','c']})\n
from tablite import Table ## To create a tablite table is as simple as populating a dictionary: t = Table({'A':[1,2,3], 'B':['a','b','c']}) In\u00a0[2]: Copied!
## In this notebook we can show tables in the HTML style:\nt\n
## In this notebook we can show tables in the HTML style: t Out[2]: #AB 01a 12b 23c In\u00a0[3]: Copied!
## or the ascii style:\nt.show()\n
## or the ascii style: t.show()
+==+=+=+\n|# |A|B|\n+--+-+-+\n| 0|1|a|\n| 1|2|b|\n| 2|3|c|\n+==+=+=+\n
In\u00a0[4]: Copied!
## or if you'd like to inspect the table, use:\nprint(str(t))\n
## or if you'd like to inspect the table, use: print(str(t))
Table(2 columns, 3 rows)\n
In\u00a0[5]: Copied!
## You can also add all columns at once (slower) if you prefer. \nt2 = Table(headers=('A','B'), rows=((1,'a'),(2,'b'),(3,'c')))\nassert t==t2\n
## You can also add all columns at once (slower) if you prefer. t2 = Table(headers=('A','B'), rows=((1,'a'),(2,'b'),(3,'c'))) assert t==t2 In\u00a0[6]: Copied!
## or load data:\nt3 = Table.from_file('tests/data/book1.csv')\n\n## to view any table in the notebook just let jupyter show the table. If you're using the terminal use .show(). \n## Note that show gives either first and last 7 rows or the whole table if it is less than 20 rows.\nt3\n
## or load data: t3 = Table.from_file('tests/data/book1.csv') ## to view any table in the notebook just let jupyter show the table. If you're using the terminal use .show(). ## Note that show gives either first and last 7 rows or the whole table if it is less than 20 rows. t3
Collecting tasks: 'tests/data/book1.csv'\nDumping tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 487.82it/s]\n
Out[6]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[7]: Copied!
## should you however want to select the headers instead of importing everything\n## (which maybe timeconsuming), simply use get_headers(path)\nfrom tablite.tools import get_headers\nfrom pathlib import Path\npath = Path('tests/data/book1.csv')\nsample = get_headers(path, linecount=5)\nprint(f\"sample is of type {type(sample)} and has the following entries:\")\nfor k,v in sample.items():\n    print(k)\n    if isinstance(v,list):\n        for r in sample[k]:\n            print(\"\\t\", r)\n
## should you however want to select the headers instead of importing everything ## (which maybe timeconsuming), simply use get_headers(path) from tablite.tools import get_headers from pathlib import Path path = Path('tests/data/book1.csv') sample = get_headers(path, linecount=5) print(f\"sample is of type {type(sample)} and has the following entries:\") for k,v in sample.items(): print(k) if isinstance(v,list): for r in sample[k]: print(\"\\t\", r)
sample is of type <class 'dict'> and has the following entries:\ndelimiter\nbook1.csv\n\t ['a', 'b', 'c', 'd', 'e', 'f']\n\t ['1', '0.060606061', '0.090909091', '0.121212121', '0.151515152', '0.181818182']\n\t ['2', '0.121212121', '0.242424242', '0.484848485', '0.96969697', '1.939393939']\n\t ['3', '0.242424242', '0.484848485', '0.96969697', '1.939393939', '3.878787879']\n\t ['4', '0.484848485', '0.96969697', '1.939393939', '3.878787879', '7.757575758']\n\t ['5', '0.96969697', '1.939393939', '3.878787879', '7.757575758', '15.51515152']\n
In\u00a0[8]: Copied!
## to extend a table by adding columns, use t[new] = [new values]\nt['C'] = [4,5,6]\n## but make sure the column has the same length as the rest of the table!\nt\n
## to extend a table by adding columns, use t[new] = [new values] t['C'] = [4,5,6] ## but make sure the column has the same length as the rest of the table! t Out[8]: #ABC 01a4 12b5 23c6 In\u00a0[9]: Copied!
## should you want to mix datatypes, tablite will not complain:\nfrom datetime import datetime, date,time,timedelta\nimport numpy as np\n## What you put in ...\nt4 = Table()\nt4['mixed'] = [\n    -1,0,1,  # regular integers\n    -12345678909876543211234567890987654321,  # very very large integer\n    None,np.nan,  # null values \n    \"one\", \"\",  # strings\n    True,False,  # booleans\n    float('inf'), 0.01,  # floats\n    date(2000,1,1),   # date\n    datetime(2002,2,3,23,0,4,6660),  # datetime\n    time(12,12,12),  # time\n    timedelta(days=3, seconds=5678)  # timedelta\n]\n## ... is exactly what you get out:\nt4\n
## should you want to mix datatypes, tablite will not complain: from datetime import datetime, date,time,timedelta import numpy as np ## What you put in ... t4 = Table() t4['mixed'] = [ -1,0,1, # regular integers -12345678909876543211234567890987654321, # very very large integer None,np.nan, # null values \"one\", \"\", # strings True,False, # booleans float('inf'), 0.01, # floats date(2000,1,1), # date datetime(2002,2,3,23,0,4,6660), # datetime time(12,12,12), # time timedelta(days=3, seconds=5678) # timedelta ] ## ... is exactly what you get out: t4 Out[9]: #mixed 0-1 10 21 3-12345678909876543211234567890987654321 4None 5nan 6one 7 8True 9False10inf110.01122000-01-01132002-02-03 23:00:04.0066601412:12:12153 days, 1:34:38 In\u00a0[10]: Copied!
## also if you claim the values back as a python list:\nfor item in list(t4['mixed']):\n    print(item)\n
## also if you claim the values back as a python list: for item in list(t4['mixed']): print(item)
-1\n0\n1\n-12345678909876543211234567890987654321\nNone\nnan\none\n\nTrue\nFalse\ninf\n0.01\n2000-01-01\n2002-02-03 23:00:04.006660\n12:12:12\n3 days, 1:34:38\n

The column itself (__repr__) shows us the pid, file location and the entries, so you know exactly what you're working with.

In\u00a0[11]: Copied!
t4['mixed']\n
t4['mixed'] Out[11]:
Column(/tmp/tablite-tmp/pid-54911, [-1 0 1 -12345678909876543211234567890987654321 None nan 'one' '' True\n False inf 0.01 datetime.date(2000, 1, 1)\n datetime.datetime(2002, 2, 3, 23, 0, 4, 6660) datetime.time(12, 12, 12)\n datetime.timedelta(days=3, seconds=5678)])
In\u00a0[12]: Copied!
## to view the datatypes in a column, use Column.types()\ntype_dict = t4['mixed'].types()\nfor k,v in type_dict.items():\n    print(k,v)\n
## to view the datatypes in a column, use Column.types() type_dict = t4['mixed'].types() for k,v in type_dict.items(): print(k,v)
<class 'int'> 4\n<class 'NoneType'> 1\n<class 'float'> 3\n<class 'str'> 2\n<class 'bool'> 2\n<class 'datetime.date'> 1\n<class 'datetime.datetime'> 1\n<class 'datetime.time'> 1\n<class 'datetime.timedelta'> 1\n
In\u00a0[13]: Copied!
## You may have noticed that all datatypes in t3 where identified as floats, despite their origin from a text type file.\n## This is because tablite guesses the most probable datatype using the `.guess` function on each column.\n## You can use the .guess function like this:\nfrom tablite import DataTypes\nt3['a'] = DataTypes.guess(t3['a'])\n## You can also convert the datatype using a list comprehension\nt3['b'] = [float(v) for v in t3['b']]\nt3\n
## You may have noticed that all datatypes in t3 where identified as floats, despite their origin from a text type file. ## This is because tablite guesses the most probable datatype using the `.guess` function on each column. ## You can use the .guess function like this: from tablite import DataTypes t3['a'] = DataTypes.guess(t3['a']) ## You can also convert the datatype using a list comprehension t3['b'] = [float(v) for v in t3['b']] t3 Out[13]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[14]: Copied!
t = Table()\nfor column_name in 'abcde':\n    t[column_name] =[i for i in range(5)]\n
t = Table() for column_name in 'abcde': t[column_name] =[i for i in range(5)]

(2) we want to add two new columns using the functions:

In\u00a0[15]: Copied!
def f1(a,b,c):\n    return a+b+c+1\ndef f2(b,c,d):\n    return b*c*d\n
def f1(a,b,c): return a+b+c+1 def f2(b,c,d): return b*c*d

(3) and we want to compute two new columns f and g:

In\u00a0[16]: Copied!
t.add_columns('f', 'g')\n
t.add_columns('f', 'g')

(4) we can now use the filter, to iterate over the table, and add the values to the two new columns:

In\u00a0[17]: Copied!
f,g=[],[]\nfor row in t['a', 'b', 'c', 'd'].rows:\n    a, b, c, d = row\n\n    f.append(f1(a, b, c))\n    g.append(f2(b, c, d))\nt['f'] = f\nt['g'] = g\n\nassert len(t) == 5\nassert list(t.columns) == list('abcdefg')\nt\n
f,g=[],[] for row in t['a', 'b', 'c', 'd'].rows: a, b, c, d = row f.append(f1(a, b, c)) g.append(f2(b, c, d)) t['f'] = f t['g'] = g assert len(t) == 5 assert list(t.columns) == list('abcdefg') t Out[17]: #abcdefg 00000010 11111141 22222278 3333331027 4444441364

Take note that if your dataset is assymmetric, a warning will be show:

In\u00a0[18]: Copied!
assymmetric_table = Table({'a':[1,2,3], 'b':[1,2]})\nfor row in assymmetric_table.rows:\n    print(row)\n## warning at the bottom ---v\n
assymmetric_table = Table({'a':[1,2,3], 'b':[1,2]}) for row in assymmetric_table.rows: print(row) ## warning at the bottom ---v
[1, 1]\n[2, 2]\n[3, None]\n
/home/bjorn/github/tablite/tablite/base.py:1188: UserWarning: Column b has length 2 / 3. None will appear as fill value.\n  warnings.warn(f\"Column {name} has length {len(column)} / {n_max}. None will appear as fill value.\")\n
In\u00a0[19]: Copied!
table7 = Table(columns={\n'A': [1,1,2,2,3,4],\n'B': [1,1,2,2,30,40],\n'C': [-1,-2,-3,-4,-5,-6]\n})\nindex = table7.index('A', 'B')\nfor k, v in index.items():\n    print(\"key\", k, \"indices\", v)\n
table7 = Table(columns={ 'A': [1,1,2,2,3,4], 'B': [1,1,2,2,30,40], 'C': [-1,-2,-3,-4,-5,-6] }) index = table7.index('A', 'B') for k, v in index.items(): print(\"key\", k, \"indices\", v)
key (1, 1) indices [0, 1]\nkey (2, 2) indices [2, 3]\nkey (3, 30) indices [4]\nkey (4, 40) indices [5]\n

The keys are created for each unique column-key-pair, and the value is the index where the key is found. To fetch all rows for key (2,2), we can use:

In\u00a0[20]: Copied!
for ix, row in enumerate(table7.rows):\n    if ix in index[(2,2)]:\n        print(row)\n
for ix, row in enumerate(table7.rows): if ix in index[(2,2)]: print(row)
[2, 2, -3]\n[2, 2, -4]\n
In\u00a0[21]: Copied!
## to append one table to another, use + or += \nprint('length before:', len(t3))  # length before: 45\nt5 = t3 + t3  \nprint('length after +', len(t5))  # length after + 90\nt5 += t3 \nprint('length after +=', len(t5))  # length after += 135\n## if you need a lot of numbers for a test, you can repeat a table using * and *=\nt5 *= 1_000\nprint('length after +=', len(t5))  # length after += 135000\n
## to append one table to another, use + or += print('length before:', len(t3)) # length before: 45 t5 = t3 + t3 print('length after +', len(t5)) # length after + 90 t5 += t3 print('length after +=', len(t5)) # length after += 135 ## if you need a lot of numbers for a test, you can repeat a table using * and *= t5 *= 1_000 print('length after +=', len(t5)) # length after += 135000
length before: 45\nlength after + 90\nlength after += 135\nlength after += 135000\n
In\u00a0[22]: Copied!
t5\n
t5 Out[22]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606..................... 134,9933916659267088.033318534175.066637068350.0133274000000.0266548000000.0 134,9944033318534175.066637068350.0133274000000.0266548000000.0533097000000.0 134,9954166637068350.0133274000000.0266548000000.0533097000000.01066190000000.0 134,99642133274000000.0266548000000.0533097000000.01066190000000.02132390000000.0 134,99743266548000000.0533097000000.01066190000000.02132390000000.04264770000000.0 134,99844533097000000.01066190000000.02132390000000.04264770000000.08529540000000.0 134,999451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0 In\u00a0[23]: Copied!
## if your are in doubt whether your tables will be the same you can use .stack(other)\nassert t.columns != t2.columns  # compares list of column names.\nt6 = t.stack(t2)\nt6\n
## if your are in doubt whether your tables will be the same you can use .stack(other) assert t.columns != t2.columns # compares list of column names. t6 = t.stack(t2) t6 Out[23]: #abcdefgAB 00000010NoneNone 11111141NoneNone 22222278NoneNone 3333331027NoneNone 4444441364NoneNone 5NoneNoneNoneNoneNoneNoneNone1a 6NoneNoneNoneNoneNoneNoneNone2b 7NoneNoneNoneNoneNoneNoneNone3c In\u00a0[24]: Copied!
## As you can see above, t6['C'] is padded with \"None\" where t2 was missing the columns.\n\n## if you need a more detailed view of the columns you can iterate:\nfor name in t.columns:\n    col_from_t = t[name]\n    if name in t2.columns:\n        col_from_t2 = t2[name]\n        print(name, col_from_t == col_from_t2)\n    else:\n        print(name, \"not in t2\")\n
## As you can see above, t6['C'] is padded with \"None\" where t2 was missing the columns. ## if you need a more detailed view of the columns you can iterate: for name in t.columns: col_from_t = t[name] if name in t2.columns: col_from_t2 = t2[name] print(name, col_from_t == col_from_t2) else: print(name, \"not in t2\")
a not in t2\nb not in t2\nc not in t2\nd not in t2\ne not in t2\nf not in t2\ng not in t2\n
In\u00a0[25]: Copied!
## to make a copy of a table, use table.copy()\nt3_copy = t3.copy()\n\n## you can also perform multi criteria selections using getitem [ ... ]\nt3_slice = t3['a','b','d', 5:25:5]\nt3_slice\n
## to make a copy of a table, use table.copy() t3_copy = t3.copy() ## you can also perform multi criteria selections using getitem [ ... ] t3_slice = t3['a','b','d', 5:25:5] t3_slice Out[25]: #abd 061.9393939397.757575758 11162.06060606248.2424242 2161985.9393947943.757576 32163550.06061254200.2424 In\u00a0[26]: Copied!
##deleting items also works the same way:\ndel t3_slice[1:3]  # delete row number 2 & 3 \nt3_slice\n
##deleting items also works the same way: del t3_slice[1:3] # delete row number 2 & 3 t3_slice Out[26]: #abd 061.9393939397.757575758 12163550.06061254200.2424 In\u00a0[27]: Copied!
## to wipe a table, use .clear:\nt3_slice.clear()\nt3_slice\n
## to wipe a table, use .clear: t3_slice.clear() t3_slice Out[27]: Empty Table In\u00a0[28]: Copied!
## tablite uses .npy for storage because it is fast.\n## this means you can make a table persistent using .save\nlocal_file = Path(\"local_file.tpz\")\nt5.save(local_file)\n\nold_t5 = Table.load(local_file)\nprint(\"the t5 table had\", len(old_t5), \"rows\")  # the t5 table had 135000 rows\n\ndel old_t5  # only removes the in-memory object\n\nprint(\"old_t5 still exists?\", local_file.exists())\nprint(\"path:\", local_file)\n\nimport os\nos.remove(local_file)\n
## tablite uses .npy for storage because it is fast. ## this means you can make a table persistent using .save local_file = Path(\"local_file.tpz\") t5.save(local_file) old_t5 = Table.load(local_file) print(\"the t5 table had\", len(old_t5), \"rows\") # the t5 table had 135000 rows del old_t5 # only removes the in-memory object print(\"old_t5 still exists?\", local_file.exists()) print(\"path:\", local_file) import os os.remove(local_file)
loading 'local_file.tpz' file:  55%|\u2588\u2588\u2588\u2588\u2588\u258d    | 9851/18000 [00:02<00:01, 4386.96it/s]
loading 'local_file.tpz' file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 18000/18000 [00:04<00:00, 4417.27it/s]\n
the t5 table had 135000 rows\nold_t5 still exists? True\npath: local_file.tpz\n

If you want to save a table from one session to another use save=True. This tells the garbage collector to leave the tablite Table on disk, so you can load it again without changing your code.

For example:

First time you run t = Table.import_file(....big.csv) it may take a minute or two.

If you then add t.save=True and restart python, the second time you run t = Table.import_file(....big.csv) it will take a few milliseconds instead of minutes.

In\u00a0[29]: Copied!
unfiltered = Table({'a':[1,2,3,4], 'b':[10,20,30,40]})\n
unfiltered = Table({'a':[1,2,3,4], 'b':[10,20,30,40]}) In\u00a0[30]: Copied!
true,false = unfiltered.filter(\n    [\n        {\"column1\": 'a', \"criteria\":\">=\", 'value2':3}\n    ], filter_type='all'\n)\n
true,false = unfiltered.filter( [ {\"column1\": 'a', \"criteria\":\">=\", 'value2':3} ], filter_type='all' ) In\u00a0[31]: Copied!
true\n
true Out[31]: #ab 0330 1440 In\u00a0[32]: Copied!
false.show()  # using show here to show that terminal users can have a nice view too.\n
false.show() # using show here to show that terminal users can have a nice view too.
+==+=+==+\n|# |a|b |\n+--+-+--+\n| 0|1|10|\n| 1|2|20|\n+==+=+==+\n
In\u00a0[33]: Copied!
ty = Table({'a':[1,2,3,4],'b': [10,20,30,40]})\n
ty = Table({'a':[1,2,3,4],'b': [10,20,30,40]}) In\u00a0[34]: Copied!
## typical python\nany(i > 3 for i in ty['a'])\n
## typical python any(i > 3 for i in ty['a']) Out[34]:
True
In\u00a0[35]: Copied!
## hereby you can do:\nany( ty.any(**{'a':lambda x:x>3}).rows )\n
## hereby you can do: any( ty.any(**{'a':lambda x:x>3}).rows ) Out[35]:
True
In\u00a0[36]: Copied!
## if you have multiple criteria this also works:\nall( ty.all(**{'a': lambda x:x>=2, 'b': lambda x:x<=30}).rows )\n
## if you have multiple criteria this also works: all( ty.all(**{'a': lambda x:x>=2, 'b': lambda x:x<=30}).rows ) Out[36]:
True
In\u00a0[37]: Copied!
## or this if you want to see the table.\nty.all(a=lambda x:x>2, b=lambda x:x<=30)\n
## or this if you want to see the table. ty.all(a=lambda x:x>2, b=lambda x:x<=30) Out[37]: #ab 0330 In\u00a0[38]: Copied!
## As `all` and `any` returns tables, this also means that you can chain operations:\nty.any(a=lambda x:x>2).any(b=30)\n
## As `all` and `any` returns tables, this also means that you can chain operations: ty.any(a=lambda x:x>2).any(b=30) Out[38]: #ab 0330 In\u00a0[39]: Copied!
table = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9],\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10],\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0],\n})\ntable\n
table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9], 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10], 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0], }) table Out[39]: #ABC 01100 1None1001 2810 3311 4410 5611 65100 77101 89100 In\u00a0[40]: Copied!
sort_order = {'B': False, 'C': False, 'A': False}\nassert not table.is_sorted(mapping=sort_order)\n\nsorted_table = table.sort(mapping=sort_order)\nsorted_table\n
sort_order = {'B': False, 'C': False, 'A': False} assert not table.is_sorted(mapping=sort_order) sorted_table = table.sort(mapping=sort_order) sorted_table
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2719.45it/s]\ncreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 3434.20it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 1902.47it/s]\n

Sort is reasonable effective as it uses multiprocessing above a million fields.

Hint: You can set this limit in tablite.config, like this:

In\u00a0[41]: Copied!
from tablite.config import Config\nprint(f\"multiprocessing is used above {Config.SINGLE_PROCESSING_LIMIT:,} fields\")\n
from tablite.config import Config print(f\"multiprocessing is used above {Config.SINGLE_PROCESSING_LIMIT:,} fields\")
multiprocessing is used above 1,000,000 fields\n
In\u00a0[42]: Copied!
import math\nn = math.ceil(1_000_000 / (9*3))\n\ntable = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9]*n,\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n,\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0]*n,\n})\ntable\n
import math n = math.ceil(1_000_000 / (9*3)) table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9]*n, 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n, 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0]*n, }) table Out[42]: #ABC 01100 1None1001 2810 3311 4410 5611 65100............ 333,335810 333,336311 333,337410 333,338611 333,3395100 333,3407101 333,3419100 In\u00a0[43]: Copied!
import time as cputime\nstart = cputime.time()\nsort_order = {'B': False, 'C': False, 'A': False}\nsorted_table = table.sort(mapping=sort_order)  # sorts 1M values.\nprint(\"table sorting took \", round(cputime.time() - start,3), \"secs\")\nsorted_table\n
import time as cputime start = cputime.time() sort_order = {'B': False, 'C': False, 'A': False} sorted_table = table.sort(mapping=sort_order) # sorts 1M values. print(\"table sorting took \", round(cputime.time() - start,3), \"secs\") sorted_table
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00,  4.20it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 18.17it/s]
table sorting took  0.913 secs\n
\n
In\u00a0[44]: Copied!
n = math.ceil(1_000_000 / (9*3))\n\ntable = Table({\n    'A':[ 1, None, 8, 3, 4, 6,  5,  7,  9]*n,\n    'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n,\n    'C':[ 0,    1, 0, 1, 0, 1,  0,  1,  0]*n,\n})\ntable\n
n = math.ceil(1_000_000 / (9*3)) table = Table({ 'A':[ 1, None, 8, 3, 4, 6, 5, 7, 9]*n, 'B':[10,'100', 1, 1, 1, 1, 10, 10, 10]*n, 'C':[ 0, 1, 0, 1, 0, 1, 0, 1, 0]*n, }) table Out[44]: #ABC 01100 1None1001 2810 3311 4410 5611 65100............ 333,335810 333,336311 333,337410 333,338611 333,3395100 333,3407101 333,3419100 In\u00a0[45]: Copied!
from tablite import GroupBy as gb\ngrpby = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)])\ngrpby\n
from tablite import GroupBy as gb grpby = table.groupby(keys=['C', 'B'], functions=[('A', gb.count)]) grpby
groupby: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 333342/333342 [00:00<00:00, 427322.50it/s]\n
Out[45]: #CBCount(A) 0010111114 1110037038 20174076 31174076 411037038

Here is the list of groupby functions:

class GroupBy(object):    \n    max = Max  # shortcuts to avoid having to type a long list of imports.\n    min = Min\n    sum = Sum\n    product = Product\n    first = First\n    last = Last\n    count = Count\n    count_unique = CountUnique\n    avg = Average\n    stdev = StandardDeviation\n    median = Median\n    mode = Mode\n
In\u00a0[46]: Copied!
t = Table({\n    'A':[1, 1, 2, 2, 3, 3] * 2,\n    'B':[1, 2, 3, 4, 5, 6] * 2,\n    'C':[6, 5, 4, 3, 2, 1] * 2,\n})\nt\n
t = Table({ 'A':[1, 1, 2, 2, 3, 3] * 2, 'B':[1, 2, 3, 4, 5, 6] * 2, 'C':[6, 5, 4, 3, 2, 1] * 2, }) t Out[46]: #ABC 0116 1125 2234 3243 4352 5361 6116 7125 8234 92431035211361 In\u00a0[47]: Copied!
t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False)\nt2\n
t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum), ('B', gb.count)], values_as_rows=False) t2
pivot: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 14/14 [00:00<00:00, 3643.83it/s]\n
Out[47]: #CSum(B,A=1)Count(B,A=1)Sum(B,A=2)Count(B,A=2)Sum(B,A=3)Count(B,A=3) 0622NoneNoneNoneNone 1542NoneNoneNoneNone 24NoneNone62NoneNone 33NoneNone82NoneNone 42NoneNoneNoneNone102 51NoneNoneNoneNone122 In\u00a0[48]: Copied!
numbers = Table()\nnumbers.add_column('number', data=[      1,      2,       3,       4,   None])\nnumbers.add_column('colour', data=['black', 'blue', 'white', 'white', 'blue'])\n\nletters = Table()\nletters.add_column('letter', data=[  'a',     'b',      'c',     'd',   None])\nletters.add_column('color', data=['blue', 'white', 'orange', 'white', 'blue'])\n
numbers = Table() numbers.add_column('number', data=[ 1, 2, 3, 4, None]) numbers.add_column('colour', data=['black', 'blue', 'white', 'white', 'blue']) letters = Table() letters.add_column('letter', data=[ 'a', 'b', 'c', 'd', None]) letters.add_column('color', data=['blue', 'white', 'orange', 'white', 'blue']) In\u00a0[49]: Copied!
## left join\n## SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\nleft_join = numbers.left_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\nleft_join\n
## left join ## SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color left_join = numbers.left_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) left_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1221.94it/s]\n
Out[49]: #numberletter 01None 12a 22None 3Nonea 4NoneNone 53b 63d 74b 84d In\u00a0[50]: Copied!
## inner join\n## SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\ninner_join = numbers.inner_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\ninner_join\n
## inner join ## SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color inner_join = numbers.inner_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) inner_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1121.77it/s]\n
Out[50]: #numberletter 02a 12None 2Nonea 3NoneNone 43b 53d 64b 74d In\u00a0[51]: Copied!
# outer join\n## SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\nouter_join = numbers.outer_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'])\nouter_join\n
# outer join ## SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color outer_join = numbers.outer_join(letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']) outer_join
join: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1585.15it/s]\n
Out[51]: #numberletter 01None 12a 22None 3Nonea 4NoneNone 53b 63d 74b 84d 9Nonec

Q: But ...I think there's a bug in the join... A: Venn diagrams do not explain joins.

A Venn diagram is a widely-used diagram style that shows the logical relation between sets, popularised by John Venn in the 1880s. The diagrams are used to teach elementary set theory, and to illustrate simple set relationshipssource: en.wikipedia.org

Joins operate over rows and when there are duplicate rows, these will be replicated in the output. Many beginners are surprised by this, because they didn't read the SQL standard.

Q: So what do I do? A: If you want to get rid of duplicates using tablite, use the index functionality across all columns and pick the first row from each index. Here's the recipe that starts with plenty of duplicates:

In\u00a0[52]: Copied!
old_table = Table({\n'A':[1,1,1,2,2,2,3,3,3],\n'B':[1,1,4,2,2,5,3,3,6],\n})\nold_table\n
old_table = Table({ 'A':[1,1,1,2,2,2,3,3,3], 'B':[1,1,4,2,2,5,3,3,6], }) old_table Out[52]: #AB 011 111 214 322 422 525 633 733 836 In\u00a0[53]: Copied!
## CREATE TABLE OF UNIQUE ENTRIES (a.k.a. DEDUPLICATE)\nnew_table = old_table.drop_duplicates()\nnew_table\n
## CREATE TABLE OF UNIQUE ENTRIES (a.k.a. DEDUPLICATE) new_table = old_table.drop_duplicates() new_table
9it [00:00, 11329.15it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1819.26it/s]\n
Out[53]: #AB 011 114 222 325 433 536

You can also use groupby; We'll get to that in a minute.

Lookup is a special case of a search loop: Say for example you are planning a concert and want to make sure that your friends can make it home using public transport: You would have to find the first departure after the concert ends towards their home. A join would only give you a direct match on the time.

Lookup allows you \"to iterate through a list of data and find the first match given a set of criteria.\"

Here's an example:

First we have our list of friends and their stops.

In\u00a0[54]: Copied!
friends = Table({\n\"name\":['Alice', 'Betty', 'Charlie', 'Dorethy', 'Edward', 'Fred'],\n\"stop\":['Downtown-1', 'Downtown-2', 'Hillside View', 'Hillside Crescent', 'Downtown-2', 'Chicago'],\n})\nfriends\n
friends = Table({ \"name\":['Alice', 'Betty', 'Charlie', 'Dorethy', 'Edward', 'Fred'], \"stop\":['Downtown-1', 'Downtown-2', 'Hillside View', 'Hillside Crescent', 'Downtown-2', 'Chicago'], }) friends Out[54]: #namestop 0AliceDowntown-1 1BettyDowntown-2 2CharlieHillside View 3DorethyHillside Crescent 4EdwardDowntown-2 5FredChicago

Next we need a list of bus routes and their time and stops. I don't have that, so I'm making one up:

In\u00a0[55]: Copied!
import random\nrandom.seed(11)\ntable_size = 40\n\ntimes = [DataTypes.time(random.randint(21, 23), random.randint(0, 59)) for i in range(table_size)]\nstops = ['Stadium', 'Hillside', 'Hillside View', 'Hillside Crescent', 'Downtown-1', 'Downtown-2',\n            'Central station'] * 2 + [f'Random Road-{i}' for i in range(table_size)]\nroute = [random.choice([1, 2, 3]) for i in stops]\n
import random random.seed(11) table_size = 40 times = [DataTypes.time(random.randint(21, 23), random.randint(0, 59)) for i in range(table_size)] stops = ['Stadium', 'Hillside', 'Hillside View', 'Hillside Crescent', 'Downtown-1', 'Downtown-2', 'Central station'] * 2 + [f'Random Road-{i}' for i in range(table_size)] route = [random.choice([1, 2, 3]) for i in stops] In\u00a0[56]: Copied!
bus_table = Table({\n\"time\":times,\n\"stop\":stops[:table_size],\n\"route\":route[:table_size],\n})\nbus_table.sort(mapping={'time': False})\n\nprint(\"Departures from Concert Hall towards ...\")\nbus_table[0:10]\n
bus_table = Table({ \"time\":times, \"stop\":stops[:table_size], \"route\":route[:table_size], }) bus_table.sort(mapping={'time': False}) print(\"Departures from Concert Hall towards ...\") bus_table[0:10]
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 1459.90it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2421.65it/s]\n
Departures from Concert Hall towards ...\n
Out[56]: #timestoproute 021:02:00Random Road-62 121:05:00Hillside Crescent2 221:06:00Hillside1 321:25:00Random Road-241 421:29:00Random Road-161 521:32:00Random Road-211 621:33:00Random Road-121 721:36:00Random Road-233 821:38:00Central station2 921:38:00Random Road-82

Let's say the concerts ends at 21:00 and it takes a 10 minutes to get to the bus-stop. Earliest departure must then be 21:10 - goodbye hugs included.

In\u00a0[57]: Copied!
lookup_1 = friends.lookup(bus_table, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop'))\nlookup1_sorted = lookup_1.sorted(mapping={'time': False, 'name':False}, sort_mode='unix')\nlookup1_sorted\n
lookup_1 = friends.lookup(bus_table, (DataTypes.time(21, 10), \"<=\", 'time'), ('stop', \"==\", 'stop')) lookup1_sorted = lookup_1.sorted(mapping={'time': False, 'name':False}, sort_mode='unix') lookup1_sorted
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 6/6 [00:00<00:00, 1513.92it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 3/3 [00:00<00:00, 2003.65it/s]\ncreating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 2589.88it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 5/5 [00:00<00:00, 2034.29it/s]\n
Out[57]: #namestoptimestop_1route 0FredChicagoNoneNoneNone 1BettyDowntown-221:51:00Downtown-21 2EdwardDowntown-221:51:00Downtown-21 3CharlieHillside View22:19:00Hillside View2 4AliceDowntown-123:12:00Downtown-13 5DorethyHillside Crescent23:54:00Hillside Crescent1

Lookup's ability to custom criteria is thereby far more versatile than SQL joins.

But with great power comes great responsibility.

In\u00a0[58]: Copied!
materials = Table({\n    'bom_id': [1, 2, 3, 4, 5, 6, 7, 8, 9], \n    'partial_of': [1, 2, 3, 4, 5, 6, 7, 4, 6], \n    'sku': ['A', 'irrelevant', 'empty carton', 'pkd carton', 'empty pallet', 'pkd pallet', 'pkd irrelevant', 'ppkd carton', 'ppkd pallet'], \n    'material_id': [None, None, None, 3, None, 5, 3, 3, 5], \n    'quantity': [10, 20, 30, 40, 50, 60, 70, 80, 90]\n})\n    # 9 is a partially packed pallet of 6\n\n## multiple values.\nlooking_for = Table({\n    'bom_id': [3,4,6], \n    'moq': [1,2,3]\n    })\n
materials = Table({ 'bom_id': [1, 2, 3, 4, 5, 6, 7, 8, 9], 'partial_of': [1, 2, 3, 4, 5, 6, 7, 4, 6], 'sku': ['A', 'irrelevant', 'empty carton', 'pkd carton', 'empty pallet', 'pkd pallet', 'pkd irrelevant', 'ppkd carton', 'ppkd pallet'], 'material_id': [None, None, None, 3, None, 5, 3, 3, 5], 'quantity': [10, 20, 30, 40, 50, 60, 70, 80, 90] }) # 9 is a partially packed pallet of 6 ## multiple values. looking_for = Table({ 'bom_id': [3,4,6], 'moq': [1,2,3] })

Our goals is now to find the quantity from the materials table based on the items in the looking_for table.

This requires two steps:

  1. lookup
  2. filter for all by dropping items that didn't match.
In\u00a0[59]: Copied!
## step 1/2:\nproducts_lookup = materials.lookup(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"), all=False)   \nproducts_lookup\n
## step 1/2: products_lookup = materials.lookup(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"), all=False) products_lookup
100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 9/9 [00:00<00:00, 3651.81it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1625.38it/s]\n
Out[59]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 011ANone10NoneNone 122irrelevantNone20NoneNone 233empty cartonNone3031 344pkd carton34042 455empty palletNone50NoneNone 566pkd pallet56063 677pkd irrelevant370NoneNone 784ppkd carton38042 896ppkd pallet59063 In\u00a0[60]: Copied!
## step 2/2:\nproducts = products_lookup.all(bom_id_1=lambda x: x is not None)\nproducts\n
## step 2/2: products = products_lookup.all(bom_id_1=lambda x: x is not None) products Out[60]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 033empty cartonNone3031 144pkd carton34042 266pkd pallet56063 384ppkd carton38042 496ppkd pallet59063

The faster way to solve this problem is to use match!

Here is the example:

In\u00a0[61]: Copied!
products_matched = materials.match(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\"))\nproducts_matched\n
products_matched = materials.match(looking_for, (\"bom_id\", \"==\", \"bom_id\"), (\"partial_of\", \"==\", \"bom_id\")) products_matched Out[61]: #bom_idpartial_ofskumaterial_idquantitybom_id_1moq 033empty cartonNone3031 144pkd carton34042 266pkd pallet56063 384ppkd carton38042 496ppkd pallet59063 In\u00a0[62]: Copied!
assert products == products_matched\n
assert products == products_matched In\u00a0[63]: Copied!
from tablite import Table\nt = Table()  # create table\nt.add_columns('row','A','B','C')  # add columns\n
from tablite import Table t = Table() # create table t.add_columns('row','A','B','C') # add columns

The following examples are all valid and append the row (1,2,3) to the table.

In\u00a0[64]: Copied!
t.add_rows(1, 1, 2, 3)  # individual values\nt.add_rows([2, 1, 2, 3])  # list of values\nt.add_rows((3, 1, 2, 3))  # tuple of values\nt.add_rows(*(4, 1, 2, 3))  # unpacked tuple\nt.add_rows(row=5, A=1, B=2, C=3)   # keyword - args\nt.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})  # dict / json.\n
t.add_rows(1, 1, 2, 3) # individual values t.add_rows([2, 1, 2, 3]) # list of values t.add_rows((3, 1, 2, 3)) # tuple of values t.add_rows(*(4, 1, 2, 3)) # unpacked tuple t.add_rows(row=5, A=1, B=2, C=3) # keyword - args t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3}) # dict / json.

The following examples add two rows to the table

In\u00a0[65]: Copied!
t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))  # two (or more) tuples.\nt.add_rows([9, 1, 2, 3], [10, 4, 5, 6])  # two or more lists\nt.add_rows({'row': 11, 'A': 1, 'B': 2, 'C': 3},\n          {'row': 12, 'A': 4, 'B': 5, 'C': 6})  # two (or more) dicts as args.\nt.add_rows(*[{'row': 13, 'A': 1, 'B': 2, 'C': 3},\n            {'row': 14, 'A': 1, 'B': 2, 'C': 3}])  # list of dicts.\n
t.add_rows((7, 1, 2, 3), (8, 4, 5, 6)) # two (or more) tuples. t.add_rows([9, 1, 2, 3], [10, 4, 5, 6]) # two or more lists t.add_rows({'row': 11, 'A': 1, 'B': 2, 'C': 3}, {'row': 12, 'A': 4, 'B': 5, 'C': 6}) # two (or more) dicts as args. t.add_rows(*[{'row': 13, 'A': 1, 'B': 2, 'C': 3}, {'row': 14, 'A': 1, 'B': 2, 'C': 3}]) # list of dicts. In\u00a0[66]: Copied!
t\n
t Out[66]: #rowABC 01123 12123 23123 34123 45123 56123 67123 78456 89123 9104561011123111245612131231314123

As the row incremented from 1 in the first of these examples, and finished with row: 14, you can now see the whole table above

In\u00a0[67]: Copied!
from pathlib import Path\npath = Path('tests/data/book1.csv')\ntx = Table.from_file(path)\ntx\n
from pathlib import Path path = Path('tests/data/book1.csv') tx = Table.from_file(path) tx
Collecting tasks: 'tests/data/book1.csv'\nDumping tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 444.08it/s]\n
Out[67]: #abcdef 010.0606060610.0909090910.1212121210.1515151520.181818182 120.1212121210.2424242420.4848484850.969696971.939393939 230.2424242420.4848484850.969696971.9393939393.878787879 340.4848484850.969696971.9393939393.8787878797.757575758 450.969696971.9393939393.8787878797.75757575815.51515152 561.9393939393.8787878797.75757575815.5151515231.03030303 673.8787878797.75757575815.5151515231.0303030362.06060606.....................383916659267088.033318534175.066637068350.0133274000000.0266548000000.0394033318534175.066637068350.0133274000000.0266548000000.0533097000000.0404166637068350.0133274000000.0266548000000.0533097000000.01066190000000.04142133274000000.0266548000000.0533097000000.01066190000000.02132390000000.04243266548000000.0533097000000.01066190000000.02132390000000.04264770000000.04344533097000000.01066190000000.02132390000000.04264770000000.08529540000000.044451066190000000.02132390000000.04264770000000.08529540000000.017059100000000.0

Note that you can also add start, limit and chunk_size to the file reader. Here's an example:

In\u00a0[68]: Copied!
path = Path('tests/data/book1.csv')\ntx2 = Table.from_file(path, start=2, limit=15)\ntx2\n
path = Path('tests/data/book1.csv') tx2 = Table.from_file(path, start=2, limit=15) tx2
Collecting tasks: 'tests/data/book1.csv'\n
importing file: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 391.22it/s]
Dumping tasks: 'tests/data/book1.csv'\n
\n
Out[68]: #abcdef 030.2424242420.4848484850.969696971.9393939393.878787879 140.4848484850.969696971.9393939393.8787878797.757575758 250.969696971.9393939393.8787878797.75757575815.51515152 361.9393939393.8787878797.75757575815.5151515231.03030303 473.8787878797.75757575815.5151515231.0303030362.06060606 587.75757575815.5151515231.0303030362.06060606124.1212121 6915.5151515231.0303030362.06060606124.1212121248.2424242 71031.0303030362.06060606124.1212121248.2424242496.4848485 81162.06060606124.1212121248.2424242496.4848485992.969697 912124.1212121248.2424242496.4848485992.9696971985.9393941013248.2424242496.4848485992.9696971985.9393943971.8787881114496.4848485992.9696971985.9393943971.8787887943.7575761215992.9696971985.9393943971.8787887943.75757615887.5151513161985.9393943971.8787887943.75757615887.5151531775.030314173971.8787887943.75757615887.5151531775.030363550.06061

How good is the file_reader?

I've included all formats in the test suite that are publicly available from the Alan Turing institute, dateutils) and Python's csv reader.

What about MM-DD-YYYY formats? Some users from the US ask why the csv reader doesn't read the month-day-year format.

The answer is simple: It's not an iso8601 format. The US month-day-year format is a locale that may be used a lot in the US, but it isn't an international standard.

If you need to work with MM-DD-YYYY you will find that the file_reader will import the values as text (str). You can then reformat it with a custom function like:

In\u00a0[69]: Copied!
s = \"03-21-1998\"\nfrom datetime import date\nf = lambda s: date(int(s[-4:]), int(s[:2]), int(s[3:5]))\nf(s)\n
s = \"03-21-1998\" from datetime import date f = lambda s: date(int(s[-4:]), int(s[:2]), int(s[3:5])) f(s) Out[69]:
datetime.date(1998, 3, 21)
In\u00a0[70]: Copied!
from tablite.import_utils import file_readers\nfor k,v in file_readers.items():\n    print(k,v)\n
from tablite.import_utils import file_readers for k,v in file_readers.items(): print(k,v)
fods <function excel_reader at 0x7f36a3ef8c10>\njson <function excel_reader at 0x7f36a3ef8c10>\nhtml <function from_html at 0x7f36a3ef8b80>\nhdf5 <function from_hdf5 at 0x7f36a3ef8a60>\nsimple <function excel_reader at 0x7f36a3ef8c10>\nrst <function excel_reader at 0x7f36a3ef8c10>\nmediawiki <function excel_reader at 0x7f36a3ef8c10>\nxlsx <function excel_reader at 0x7f36a3ef8c10>\nxls <function excel_reader at 0x7f36a3ef8c10>\nxlsm <function excel_reader at 0x7f36a3ef8c10>\ncsv <function text_reader at 0x7f36a3ef9000>\ntsv <function text_reader at 0x7f36a3ef9000>\ntxt <function text_reader at 0x7f36a3ef9000>\nods <function ods_reader at 0x7f36a3ef8ca0>\n

(2) define your new file reader

In\u00a0[71]: Copied!
def my_magic_reader(path, **kwargs):   # define your new file reader.\n    print(\"do magic with {path}\")\n    return\n
def my_magic_reader(path, **kwargs): # define your new file reader. print(\"do magic with {path}\") return

(3) add it to the list of readers.

In\u00a0[72]: Copied!
file_readers['my_special_format'] = my_magic_reader\n
file_readers['my_special_format'] = my_magic_reader

The file_readers are all in tablite.core so if you intend to extend the readers, I recommend that you start here.

In\u00a0[73]: Copied!
file = Path('example.xlsx')\ntx2.to_xlsx(file)\nos.remove(file)\n
file = Path('example.xlsx') tx2.to_xlsx(file) os.remove(file)

In\u00a0[74]: Copied!
from tablite import Table\n\nt = Table({\n'a':[1, 2, 8, 3, 4, 6, 5, 7, 9],\n'b':[10, 100, 3, 4, 16, -1, 10, 10, 10],\n})\nt.sort(mapping={\"a\":False})\nt\n
from tablite import Table t = Table({ 'a':[1, 2, 8, 3, 4, 6, 5, 7, 9], 'b':[10, 100, 3, 4, 16, -1, 10, 10, 10], }) t.sort(mapping={\"a\":False}) t
creating sort index: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 1/1 [00:00<00:00, 1674.37it/s]\njoin: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 2/2 [00:00<00:00, 1701.89it/s]\n
Out[74]: #ab 0110 12100 234 3416 4510 56-1 6710 783 8910 In\u00a0[75]: Copied!
%pip install matplotlib -q\n
%pip install matplotlib -q
Note: you may need to restart the kernel to use updated packages.\n
In\u00a0[76]: Copied!
import matplotlib.pyplot as plt\nplt.plot(t['a'], t['b'])\nplt.ylabel('Hello Figure')\nplt.show()\n
import matplotlib.pyplot as plt plt.plot(t['a'], t['b']) plt.ylabel('Hello Figure') plt.show() In\u00a0[77]: Copied!
## Let's monitor the memory and record the observations into a table!\nimport psutil, os, gc\nfrom time import process_time,sleep\nprocess = psutil.Process(os.getpid())\n\ndef mem_time():  # go and check taskmanagers memory usage.\n    return process.memory_info().rss, process_time()\n\ndigits = 1_000_000\n\nrecords = Table({'method':[], 'memory':[], 'time':[]})\n
## Let's monitor the memory and record the observations into a table! import psutil, os, gc from time import process_time,sleep process = psutil.Process(os.getpid()) def mem_time(): # go and check taskmanagers memory usage. return process.memory_info().rss, process_time() digits = 1_000_000 records = Table({'method':[], 'memory':[], 'time':[]})

The row based format: 1 million 10-tuples

In\u00a0[78]: Copied!
before, start = mem_time()\nL = [tuple([11 for _ in range(10)]) for _ in range(digits)]\nafter, end = mem_time()  \ndel L\ngc.collect()\n\nrecords.add_rows(*('1e6 lists w. 10 integers', after - before, round(end-start,4)))\nrecords\n
before, start = mem_time() L = [tuple([11 for _ in range(10)]) for _ in range(digits)] after, end = mem_time() del L gc.collect() records.add_rows(*('1e6 lists w. 10 integers', after - before, round(end-start,4))) records Out[78]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045

The column based format: 10 columns with 1M values:

In\u00a0[79]: Copied!
before, start = mem_time()\nL = [[11 for i2 in range(digits)] for i1 in range(10)]\nafter,end = mem_time()\n\ndel L\ngc.collect()\nrecords.add_rows(('10 lists with 1e6 integers', after - before, round(end-start,4)))\n
before, start = mem_time() L = [[11 for i2 in range(digits)] for i1 in range(10)] after,end = mem_time() del L gc.collect() records.add_rows(('10 lists with 1e6 integers', after - before, round(end-start,4)))

We've thereby saved 50 Mb by avoiding the overhead from managing 1 million lists.

Q: But why didn't I just use an array? It would have even lower memory footprint.

A: First, array's don't handle None's and we get that frequently in dirty csv data.

Second, Table needs even less memory.

Let's try with an array:

In\u00a0[80]: Copied!
import array\n\nbefore, start = mem_time()\nL = [array.array('i', [11 for _ in range(digits)]) for _ in range(10)]\nafter,end = mem_time()\n\ndel L\ngc.collect()\nrecords.add_rows(('10 lists with 1e6 integers in arrays', after - before, round(end-start,4)))\nrecords\n
import array before, start = mem_time() L = [array.array('i', [11 for _ in range(digits)]) for _ in range(10)] after,end = mem_time() del L gc.collect() records.add_rows(('10 lists with 1e6 integers in arrays', after - before, round(end-start,4))) records Out[80]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045 110 lists with 1e6 integers752762880.1906 210 lists with 1e6 integers in arrays398336000.3633

Finally let's use a tablite.Table:

In\u00a0[81]: Copied!
before,start = mem_time()\nt = Table(columns={str(i1): [11 for i2 in range(digits)] for i1 in range(10)})\nafter,end = mem_time()\n\nrecords.add_rows(('Table with 10 columns with 1e6 integers', after - before, round(end-start,4)))\n\nbefore,start = mem_time()\nt2 = t.copy()\nafter,end = mem_time()\n\nrecords.add_rows(('2 Tables with 10 columns with 1e6 integers each', after - before, round(end-start,4)))\n\n## Let's show it, so we know nobody's cheating:\nt2\n
before,start = mem_time() t = Table(columns={str(i1): [11 for i2 in range(digits)] for i1 in range(10)}) after,end = mem_time() records.add_rows(('Table with 10 columns with 1e6 integers', after - before, round(end-start,4))) before,start = mem_time() t2 = t.copy() after,end = mem_time() records.add_rows(('2 Tables with 10 columns with 1e6 integers each', after - before, round(end-start,4))) ## Let's show it, so we know nobody's cheating: t2 Out[81]: #0123456789 011111111111111111111 111111111111111111111 211111111111111111111 311111111111111111111 411111111111111111111 511111111111111111111 611111111111111111111................................. 999,99311111111111111111111 999,99411111111111111111111 999,99511111111111111111111 999,99611111111111111111111 999,99711111111111111111111 999,99811111111111111111111 999,99911111111111111111111 In\u00a0[82]: Copied!
records\n
records Out[82]: #methodmemorytime 01e6 lists w. 10 integers1190543360.5045 110 lists with 1e6 integers752762880.1906 210 lists with 1e6 integers in arrays398336000.3633 3Table with 10 columns with 1e6 integers01.9569 42 Tables with 10 columns with 1e6 integers each00.0001

Conclusion: whilst the common worst case (1M lists with 10 integers) take up 118 Mb of RAM, Tablite's tables vanish in the noise of memory measurement.

Pandas also permits the usage of namedtuples, which are unpacked upon entry.

from collections import namedtuple\nPoint = namedtuple(\"Point\", \"x y\")\npoints = [Point(0, 0), Point(0, 3)]\npd.DataFrame(points)\n

Doing that in tablite is a bit different. To unpack the named tuple, you should do so explicitly:

t = Table({'x': [p.x for p in points], 'y': [p.y for p in points]})\n

However should you want to keep the points as namedtuple, you can do so in tablite:

t = Table()\nt['points'] = points\n

Tablite will store a serialised version of the points, so your memory overhead will be close to zero.

"},{"location":"tutorial/#tablite","title":"Tablite\u00b6","text":""},{"location":"tutorial/#introduction","title":"Introduction\u00b6","text":"

Tablite fills the data-science space where incremental data processing based on:

  • Datasets are larger than memory.
  • You don't want to worry about datatypes.

Tablite thereby competes with:

  • Pandas, but saves the memory overhead.
  • Numpy, but spares you from worrying about lower level data types
  • SQlite, by sheer speed.
  • Polars, by working beyond RAM.
  • Other libraries for data cleaning thanks to tablites powerful datatypes module.

Install: pip install tablite

Usage: >>> from tablite import Table

Upgrade: pip install tablite --no-cache --upgrade

"},{"location":"tutorial/#overview","title":"Overview\u00b6","text":"

(Version 2023.6.0 and later. For older version see this)

  • Tablite handles all Python datatypes: str, float, bool, int, date, datetime, time, timedelta and None.
  • you can select:
    • all rows in a column as table['A']
    • rows across all columns as table[4:8]
    • or a slice as table['A', 'B', slice(4,8) ].
  • you to update with table['A'][2] = new value
  • you can store or send data using json, by:
    • dumping to json: json_str = table.to_json(), or
    • you can load it with Table.from_json(json_str).
  • you can iterate over rows using for row in Table.rows.
  • you can ask column_xyz in Table.colums ?
  • load from files with new_table = Table.from_file('this.csv') which has automatic datatype detection
  • perform inner, outer & left sql join between tables as simple as table_1.inner_join(table2, keys=['A', 'B'])
  • summarise using table.groupby( ... )
  • create pivot tables using groupby.pivot( ... )
  • perform multi-criteria lookup in tables using table1.lookup(table2, criteria=.....
  • and of course a large selection of tools in from tablite.tools import *
"},{"location":"tutorial/#examples","title":"Examples\u00b6","text":"

Here are some examples:

"},{"location":"tutorial/#api-examples","title":"API Examples\u00b6","text":"

In the following sections, example are given of the Tablite API's power features:

  • Iteration
  • Append
  • Sort
  • Filter
  • Index
  • Search All
  • Search Any
  • Lookup
  • Join inner, outer,
  • GroupBy
  • Pivot table
"},{"location":"tutorial/#iteration","title":"ITERATION!\u00b6","text":"

Iteration supports for loops and list comprehension at the speed of light:

Just use [r for r in table.rows], or:

for row in table.rows:\n    row ...

Here's a more practical use case:

(1) Imagine a table with columns a,b,c,d,e (all integers) like this:

"},{"location":"tutorial/#create-index-indices","title":"Create Index / Indices\u00b6","text":"

Index supports multi-key indexing using args such as: index = table.index('B','C').

Here's an example:

"},{"location":"tutorial/#append","title":"APPEND\u00b6","text":""},{"location":"tutorial/#save","title":"SAVE\u00b6","text":""},{"location":"tutorial/#filter","title":"FILTER!\u00b6","text":""},{"location":"tutorial/#any-all","title":"Any! All?\u00b6","text":"

Any and All are cousins of the filter. They're there so you can use them in the same way as you'd use any and all in python - as boolean evaluators:

"},{"location":"tutorial/#sort","title":"SORT!\u00b6","text":""},{"location":"tutorial/#groupby","title":"GROUPBY !\u00b6","text":""},{"location":"tutorial/#did-i-say-pivot-table-yes","title":"Did I say pivot table? Yes.\u00b6","text":"

Pivot Table is included in the groupby functionality - so yes - you can pivot the groupby on any column that is used for grouping. Here's a simple example:

"},{"location":"tutorial/#join","title":"JOIN!\u00b6","text":""},{"location":"tutorial/#lookup","title":"LOOKUP!\u00b6","text":""},{"location":"tutorial/#match","title":"Match\u00b6","text":"

If you're looking to do a join where you afterwards remove the empty rows, match is the faster choice.

Here is an example.

Let's start with two tables:

"},{"location":"tutorial/#are-there-other-ways-i-can-add-data","title":"Are there other ways I can add data?\u00b6","text":"

Yes - but row based operations cause a lot of IO, so it'll work but be slower:

"},{"location":"tutorial/#okay-great-how-do-i-load-data","title":"Okay, great. How do I load data?\u00b6","text":"

Easy. Use file_reader. Here's an example:

"},{"location":"tutorial/#sweet-what-formats-are-supported-can-i-add-my-own-file-reader","title":"Sweet. What formats are supported? Can I add my own file reader?\u00b6","text":"

Yes! This is very good for special log files or custom json formats. Here's how you do it:

(1) Go to all existing readers in the tablite.core and find the closest match.

"},{"location":"tutorial/#very-nice-how-about-exporting-data","title":"Very nice. How about exporting data?\u00b6","text":"

Just use .export

"},{"location":"tutorial/#cool-does-it-play-well-with-plotting-packages","title":"Cool. Does it play well with plotting packages?\u00b6","text":"

Yes. Here's an example you can copy and paste:

"},{"location":"tutorial/#i-like-sql-can-tablite-understand-sql","title":"I like sql. Can tablite understand SQL?\u00b6","text":"

Almost. You can use table.to_sql and tablite will return ANSI-92 compliant SQL.

You can also create a table using Table.from_sql and tablite will consume ANSI-92 compliant SQL.

"},{"location":"tutorial/#but-what-do-i-do-if-im-about-to-run-out-of-memory","title":"But what do I do if I'm about to run out of memory?\u00b6","text":"

You wont. Every tablite table is backed by disk. The memory footprint of a table is only the metadata required to know the relationships between variable names and the datastructures.

Let's do a comparison:

"},{"location":"tutorial/#conclusions","title":"Conclusions\u00b6","text":"

This concludes the mega-tutorial to tablite. There's nothing more to it. But oh boy it'll save a lot of time.

Here's a summary of features:

  • Everything a list can do.
  • import csv*, fods, json, html, simple, rst, mediawiki, xlsx, xls, xlsm, csv, tsv, txt, ods using Table.from_file(...)
  • Iterate over rows or columns
  • Create multikey index, sort, use filter, any and all to select. Perform lookup across tables including using custom functions.
  • Perform multikey joins with other tables.
  • Perform groupby and reorganise data as a pivot table with max, min, sum, first, last, count, unique, average, standard deviation, median and mode.
  • Update tables with += which automatically sorts out the columns - even if they're not in perfect order.
"},{"location":"tutorial/#faq","title":"FAQ\u00b6","text":"Question Answer I'm not in a notebook. Is there a nice way to view tables? Yes. table.show() prints the ascii version I'm looking for the equivalent to apply in pandas. Just use list comprehensions: table[column] = [f(x) for x in table[column] What about map? Just use the python function: mapping = map(f, table[column name]) Is there a where function? It's called any or all like in python: table.any(column_name > 0). I like sql and sqlite. Can I use sql? Yes. Call table.to_sql() returns ANSI-92 SQL compliant table definition.You can use this in any SQL compliant engine.

| sometimes i need to clean up data with datetimes. Is there any tool to help with that? | Yes. Look at DataTypes.DataTypes.round(value, multiple) allows rounding of datetime.

"},{"location":"tutorial/#coming-to-tablite-from-pandas","title":"Coming to Tablite from Pandas\u00b6","text":"

If you're coming to Tablite from Pandas you will notice some differences.

Here's the ultra short comparison to the documentation from Pandas called 10 minutes intro to pandas

The tutorials provide the generic overview:

  • pandas tutorial
  • tablite tutorial

Some key differences

topic Tablite Viewing data Just use table.show() in print outs, or if you're in a jupyter notebook just use the variable name table Selection Slicing works both on columns and rows, and you can filter using any or all:table['A','B', 2:30:3].any(A=lambda x:x>3) to copy a table use: t2 = t.copy()This is a very fast deep copy, that has no memory overhead as tablites memory manager keeps track of the data. Missing data Tablite uses mixed column format for any format that isn't uniformTo get rid of rows with Nones and np.nans use any:table.drop_na(None, np.nan) Alternatively you can use replace: table.replace(None,5) following the syntax: table.replace_missing_values(sources, target) Operations Descriptive statistics are on a colum by column basis:table['a'].statistics() the pandas function df.apply doesn't exist in tablite. Use a list comprehension instead. For example: df.apply(np.cumsum) is just np.cumsum(t['A']) \"histogramming\" in tablite is per column: table['a'].histogram() string methods? Just use a list comprehensions: table['A', 'B'].any(A=lambda x: \"hello\" in x, B=lambda x: \"world\" in x) Merge Concatenation: Just use + or += as in t1 = t2 + t3 += t4. If the columns are out of order, tablite will sort the headers according to the order in the first table.If you're worried that the header mismatch use t1.stack(t2) Joins are ANSI92 compliant: t1.join(t2, <...args...>, join_type=...). Grouping Tablite supports multikey groupby using from tablite import Groupby as gb. table.groupby(keys, functions) Reshaping To reshape a table use transpose. to perform pivot table like operations, use: table.pivot(rows, columns, functions) subtotals aside tablite will give you everything Excels pivot table can do. Time series To convert time series use a list comprehension.t1['GMT'] = [timedelta(hours=1) + v for v in t1['date'] ] to generate a date range use:from Tablite import dateranget['date'] = date_range(start=2022/1/1, stop=2023/1/1, step=timedelta(days=1)) Categorical Pandas only seems to use this for sorting and grouping. Tablite table has .sort, .groupby and .pivot to achieve the same task. Plotting Import your favorite plotting package and feed it the values, such as:import matplotlib.pyplot as plt plt.plot(t['a'],t['b']) plt.showw() Import/Export Tablite supports the same import/export options as pandas.Tablite pegs the free memory before IO and can therefore process larger-than-RAM files. Tablite also guesses the datatypes for all ISOformats and uses multiprocessing and may therefore be faster. Should you want to inspect how guess works, use from tools import guess and try the function out. Gotchas None really. Should you come across something non-pythonic, then please post it on the issue list."},{"location":"reference/_nimlite/","title":"nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite","title":"tablite._nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite-modules","title":"Modules","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite","title":"tablite._nimlite.nimlite","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-attributes","title":"Attributes","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__doc__","title":"tablite._nimlite.nimlite.__doc__ = ''","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__file__","title":"tablite._nimlite.nimlite.__file__ = '/home/runner/work/tablite/tablite/tablite/_nimlite/nimlite.so'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__name__","title":"tablite._nimlite.nimlite.__name__ = 'tablite._nimlite.nimlite'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.__package__","title":"tablite._nimlite.nimlite.__package__ = 'tablite._nimlite'","text":"

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-classes","title":"Classes","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.NimPyException","title":"tablite._nimlite.nimlite.NimPyException","text":"

Bases: builtins.Exception

Attributes tablite._nimlite.nimlite.NimPyException.__module__ = 'nimpy'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

tablite._nimlite.nimlite.NimPyException.__weakref__ = <attribute '__weakref__' of 'NimPyException' objects>

list of weak references to the object (if defined)

"},{"location":"reference/_nimlite/#tablite._nimlite.nimlite-functions","title":"Functions","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.collect_column_select_info","title":"tablite._nimlite.nimlite.collect_column_select_info() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.do_slice_convert","title":"tablite._nimlite.nimlite.do_slice_convert() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.text_reader","title":"tablite._nimlite.nimlite.text_reader() builtin","text":""},{"location":"reference/_nimlite/#tablite._nimlite.nimlite.text_reader_task","title":"tablite._nimlite.nimlite.text_reader_task() -> None builtin","text":""},{"location":"reference/base/","title":"Base","text":""},{"location":"reference/base/#tablite.base","title":"tablite.base","text":""},{"location":"reference/base/#tablite.base-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.log","title":"tablite.base.log = logging.getLogger(__name__) module-attribute","text":""},{"location":"reference/base/#tablite.base.file_registry","title":"tablite.base.file_registry = set() module-attribute","text":""},{"location":"reference/base/#tablite.base-classes","title":"Classes","text":""},{"location":"reference/base/#tablite.base.SimplePage","title":"tablite.base.SimplePage(id, path, len, py_dtype)","text":"

Bases: object

Source code in tablite/base.py
def __init__(self, id, path, len, py_dtype) -> None:\n    self.id = id\n    self.path = Path(path) / \"pages\" / f\"{id}.npy\"\n    self.len = len\n    self.dtype = py_dtype\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.SimplePage-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.SimplePage.ids","title":"tablite.base.SimplePage.ids = count(start=1) class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.refcounts","title":"tablite.base.SimplePage.refcounts = {} class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.autocleanup","title":"tablite.base.SimplePage.autocleanup = True class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.id","title":"tablite.base.SimplePage.id = id instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.path","title":"tablite.base.SimplePage.path = Path(path) / 'pages' / f'{id}.npy' instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.len","title":"tablite.base.SimplePage.len = len instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage.dtype","title":"tablite.base.SimplePage.dtype = py_dtype instance-attribute","text":""},{"location":"reference/base/#tablite.base.SimplePage-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.SimplePage.__setstate__","title":"tablite.base.SimplePage.__setstate__(state)","text":"

when an object is unpickled, say in a case of multi-processing, object.setstate(state) is called instead of init, this means we need to update page refcount as if constructor had been called

Source code in tablite/base.py
def __setstate__(self, state):\n    \"\"\"\n    when an object is unpickled, say in a case of multi-processing,\n    object.__setstate__(state) is called instead of __init__, this means\n    we need to update page refcount as if constructor had been called\n    \"\"\"\n    self.__dict__.update(state)\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.SimplePage.next_id","title":"tablite.base.SimplePage.next_id(path) classmethod","text":"Source code in tablite/base.py
@classmethod\ndef next_id(cls, path):\n    path = Path(path)\n\n    while True:\n        _id = next(cls.ids)\n        _path = path / \"pages\" / f\"{_id}.npy\"\n\n        if not _path.exists():\n            break  # make sure we don't override existing pages if they are created outside of main thread\n\n    return _id\n
"},{"location":"reference/base/#tablite.base.SimplePage.__len__","title":"tablite.base.SimplePage.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return self.len\n
"},{"location":"reference/base/#tablite.base.SimplePage.__repr__","title":"tablite.base.SimplePage.__repr__() -> str","text":"Source code in tablite/base.py
def __repr__(self) -> str:\n    try:\n        return f\"{self.__class__.__name__}({self.path}, {self.get()})\"\n    except FileNotFoundError as e:\n        return f\"{self.__class__.__name__}({self.path}, <{type(e).__name__}>)\"\n    except Exception as e:\n        return f\"{self.__class__.__name__}({self.path}, <{e}>)\"\n
"},{"location":"reference/base/#tablite.base.SimplePage.__hash__","title":"tablite.base.SimplePage.__hash__() -> int","text":"Source code in tablite/base.py
def __hash__(self) -> int:\n    return hash(self.id)\n
"},{"location":"reference/base/#tablite.base.SimplePage.owns","title":"tablite.base.SimplePage.owns()","text":"Source code in tablite/base.py
def owns(self):\n    parts = self.path.parts\n\n    return all((p in parts for p in Path(Config.pid).parts))\n
"},{"location":"reference/base/#tablite.base.SimplePage.__del__","title":"tablite.base.SimplePage.__del__()","text":"

When python's reference count for an object is 0, python uses it's garbage collector to remove the object and free the memory. As tablite tables have columns and columns have page and pages have data stored on disk, the space on disk must be freed up as well. This del override assures the cleanup of stored data.

Source code in tablite/base.py
def __del__(self):\n    \"\"\"When python's reference count for an object is 0, python uses\n    it's garbage collector to remove the object and free the memory.\n    As tablite tables have columns and columns have page and pages have\n    data stored on disk, the space on disk must be freed up as well.\n    This __del__ override assures the cleanup of stored data.\n    \"\"\"\n    if not self.owns():\n        return\n\n    refcount = self.refcounts[self.path] = max(\n        self.refcounts.get(self.path, 0) - 1, 0\n    )\n\n    if refcount > 0:\n        return\n\n    if self.autocleanup:\n        self.path.unlink(True)\n\n    del self.refcounts[self.path]\n
"},{"location":"reference/base/#tablite.base.SimplePage.get","title":"tablite.base.SimplePage.get()","text":"

loads stored data

RETURNS DESCRIPTION

np.ndarray: stored data.

Source code in tablite/base.py
def get(self):\n    \"\"\"loads stored data\n\n    Returns:\n        np.ndarray: stored data.\n    \"\"\"\n    array = load_numpy(self.path)\n    return MetaArray(array, array.dtype, py_dtype=self.dtype)\n
"},{"location":"reference/base/#tablite.base.Page","title":"tablite.base.Page(path, array)","text":"

Bases: SimplePage

PARAMETER DESCRIPTION path

working directory.

TYPE: Path

array

data

TYPE: array

Source code in tablite/base.py
def __init__(self, path, array) -> None:\n    \"\"\"\n    Args:\n        path (Path): working directory.\n        array (np.array): data\n    \"\"\"\n    _id = self.next_id(path)\n\n    type_check(array, np.ndarray)\n\n    if Config.DISK_LIMIT <= 0:\n        pass\n    else:\n        _, _, free = shutil.disk_usage(path)\n        if free - array.nbytes < Config.DISK_LIMIT:\n            msg = \"\\n\".join(\n                [\n                    f\"Disk limit reached: Config.DISK_LIMIT = {Config.DISK_LIMIT:,} bytes.\",\n                    f\"array requires {array.nbytes:,} bytes, but only {free:,} bytes are free.\",\n                    \"To disable this check, use:\",\n                    \">>> from tablite.config import Config\",\n                    \">>> Config.DISK_LIMIT = 0\",\n                    \"To free space, clean up Config.workdir:\",\n                    f\"{Config.workdir}\",\n                ]\n            )\n            raise OSError(msg)\n\n    _len = len(array)\n    # type_check(array, MetaArray)\n    if not hasattr(array, \"metadata\"):\n        raise ValueError\n    _dtype = array.metadata[\"py_dtype\"]\n\n    super().__init__(_id, path, _len, _dtype)\n\n    np.save(self.path, array, allow_pickle=True, fix_imports=False)\n    log.debug(f\"Page saved: {self.path}\")\n
"},{"location":"reference/base/#tablite.base.Page-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Page.ids","title":"tablite.base.Page.ids = count(start=1) class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.refcounts","title":"tablite.base.Page.refcounts = {} class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.autocleanup","title":"tablite.base.Page.autocleanup = True class-attribute instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.id","title":"tablite.base.Page.id = id instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.path","title":"tablite.base.Page.path = Path(path) / 'pages' / f'{id}.npy' instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.len","title":"tablite.base.Page.len = len instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page.dtype","title":"tablite.base.Page.dtype = py_dtype instance-attribute","text":""},{"location":"reference/base/#tablite.base.Page-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Page.__setstate__","title":"tablite.base.Page.__setstate__(state)","text":"

when an object is unpickled, say in a case of multi-processing, object.setstate(state) is called instead of init, this means we need to update page refcount as if constructor had been called

Source code in tablite/base.py
def __setstate__(self, state):\n    \"\"\"\n    when an object is unpickled, say in a case of multi-processing,\n    object.__setstate__(state) is called instead of __init__, this means\n    we need to update page refcount as if constructor had been called\n    \"\"\"\n    self.__dict__.update(state)\n\n    self._incr_refcount()\n
"},{"location":"reference/base/#tablite.base.Page.next_id","title":"tablite.base.Page.next_id(path) classmethod","text":"Source code in tablite/base.py
@classmethod\ndef next_id(cls, path):\n    path = Path(path)\n\n    while True:\n        _id = next(cls.ids)\n        _path = path / \"pages\" / f\"{_id}.npy\"\n\n        if not _path.exists():\n            break  # make sure we don't override existing pages if they are created outside of main thread\n\n    return _id\n
"},{"location":"reference/base/#tablite.base.Page.__len__","title":"tablite.base.Page.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return self.len\n
"},{"location":"reference/base/#tablite.base.Page.__repr__","title":"tablite.base.Page.__repr__() -> str","text":"Source code in tablite/base.py
def __repr__(self) -> str:\n    try:\n        return f\"{self.__class__.__name__}({self.path}, {self.get()})\"\n    except FileNotFoundError as e:\n        return f\"{self.__class__.__name__}({self.path}, <{type(e).__name__}>)\"\n    except Exception as e:\n        return f\"{self.__class__.__name__}({self.path}, <{e}>)\"\n
"},{"location":"reference/base/#tablite.base.Page.__hash__","title":"tablite.base.Page.__hash__() -> int","text":"Source code in tablite/base.py
def __hash__(self) -> int:\n    return hash(self.id)\n
"},{"location":"reference/base/#tablite.base.Page.owns","title":"tablite.base.Page.owns()","text":"Source code in tablite/base.py
def owns(self):\n    parts = self.path.parts\n\n    return all((p in parts for p in Path(Config.pid).parts))\n
"},{"location":"reference/base/#tablite.base.Page.__del__","title":"tablite.base.Page.__del__()","text":"

When python's reference count for an object is 0, python uses it's garbage collector to remove the object and free the memory. As tablite tables have columns and columns have page and pages have data stored on disk, the space on disk must be freed up as well. This del override assures the cleanup of stored data.

Source code in tablite/base.py
def __del__(self):\n    \"\"\"When python's reference count for an object is 0, python uses\n    it's garbage collector to remove the object and free the memory.\n    As tablite tables have columns and columns have page and pages have\n    data stored on disk, the space on disk must be freed up as well.\n    This __del__ override assures the cleanup of stored data.\n    \"\"\"\n    if not self.owns():\n        return\n\n    refcount = self.refcounts[self.path] = max(\n        self.refcounts.get(self.path, 0) - 1, 0\n    )\n\n    if refcount > 0:\n        return\n\n    if self.autocleanup:\n        self.path.unlink(True)\n\n    del self.refcounts[self.path]\n
"},{"location":"reference/base/#tablite.base.Page.get","title":"tablite.base.Page.get()","text":"

loads stored data

RETURNS DESCRIPTION

np.ndarray: stored data.

Source code in tablite/base.py
def get(self):\n    \"\"\"loads stored data\n\n    Returns:\n        np.ndarray: stored data.\n    \"\"\"\n    array = load_numpy(self.path)\n    return MetaArray(array, array.dtype, py_dtype=self.dtype)\n
"},{"location":"reference/base/#tablite.base.Column","title":"tablite.base.Column(path, value=None)","text":"

Bases: object

Create Column

PARAMETER DESCRIPTION path

path of table.yml

TYPE: Path

value

Data to store. Defaults to None.

TYPE: Iterable DEFAULT: None

Source code in tablite/base.py
def __init__(self, path, value=None) -> None:\n    \"\"\"Create Column\n\n    Args:\n        path (Path): path of table.yml\n        value (Iterable, optional): Data to store. Defaults to None.\n    \"\"\"\n    self.path = path\n    self.pages = []  # keeps pointers to instances of Page\n    if value is not None:\n        self.extend(value)\n
"},{"location":"reference/base/#tablite.base.Column-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Column.path","title":"tablite.base.Column.path = path instance-attribute","text":""},{"location":"reference/base/#tablite.base.Column.pages","title":"tablite.base.Column.pages = [] instance-attribute","text":""},{"location":"reference/base/#tablite.base.Column-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Column.__len__","title":"tablite.base.Column.__len__()","text":"Source code in tablite/base.py
def __len__(self):\n    return sum(len(p) for p in self.pages)\n
"},{"location":"reference/base/#tablite.base.Column.__repr__","title":"tablite.base.Column.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return f\"{self.__class__.__name__}({self.path}, {self[:]})\"\n
"},{"location":"reference/base/#tablite.base.Column.repaginate","title":"tablite.base.Column.repaginate()","text":"

resizes pages to Config.PAGE_SIZE

Source code in tablite/base.py
def repaginate(self):\n    \"\"\"resizes pages to Config.PAGE_SIZE\"\"\"\n    new_pages = []\n    start, end = 0, 0\n    for _ in range(0, len(self) + 1, Config.PAGE_SIZE):\n        start, end = end, end + Config.PAGE_SIZE\n        array = self[slice(start, end, 1)]\n\n        np_dtype, py_dtype = pytype_from_iterable(array.tolist())\n        new = MetaArray(array, dtype=np_dtype, py_dtype=py_dtype)\n\n        new_pages.append(Page(self.path, new))\n    self.pages = new_pages\n
"},{"location":"reference/base/#tablite.base.Column.extend","title":"tablite.base.Column.extend(value)","text":"

extends the column.

PARAMETER DESCRIPTION value

data

TYPE: ndarray

Source code in tablite/base.py
def extend(self, value):  # USER FUNCTION.\n    \"\"\"extends the column.\n\n    Args:\n        value (np.ndarray): data\n    \"\"\"\n    if isinstance(value, Column):\n        self.pages.extend(value.pages[:])\n        return\n    elif isinstance(value, np.ndarray):\n        pass\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n    else:\n        raise TypeError(f\"Cannot extend Column with {type(value)}\")\n    type_check(value, np.ndarray)\n    for array in self._paginate(value):\n        self.pages.append(Page(path=self.path, array=array))\n
"},{"location":"reference/base/#tablite.base.Column.clear","title":"tablite.base.Column.clear()","text":"

clears the column. Like list().clear()

Source code in tablite/base.py
def clear(self):\n    \"\"\"\n    clears the column. Like list().clear()\n    \"\"\"\n    self.pages.clear()\n
"},{"location":"reference/base/#tablite.base.Column.getpages","title":"tablite.base.Column.getpages(item)","text":"

public non-user function to identify any pages + slices of data to be retrieved given a slice (item)

PARAMETER DESCRIPTION item

target slice of data

TYPE: (int, slice)

RETURNS DESCRIPTION

list of pages/np.ndarrays.

Example: [Page(1), Page(2), np.ndarray([4,5,6], int64)] This helps, for example when creating a copy, as the copy can reference the pages 1 and 2 and only need to store the np.ndarray that is unique to it.

Source code in tablite/base.py
def getpages(self, item):\n    \"\"\"public non-user function to identify any pages + slices\n    of data to be retrieved given a slice (item)\n\n    Args:\n        item (int,slice): target slice of data\n\n    Returns:\n        list of pages/np.ndarrays.\n\n    Example: [Page(1), Page(2), np.ndarray([4,5,6], int64)]\n    This helps, for example when creating a copy, as the copy\n    can reference the pages 1 and 2 and only need to store\n    the np.ndarray that is unique to it.\n    \"\"\"\n    # internal function\n    if isinstance(item, int):\n        if item < 0:\n            item = len(self) + item\n        item = slice(item, item + 1, 1)\n\n    type_check(item, slice)\n    is_reversed = False if (item.step is None or item.step > 0) else True\n\n    length = len(self)\n    scan_item = slice(*item.indices(length))\n    range_item = range(*item.indices(length))\n\n    pages = []\n    start, end = 0, 0\n    for page in self.pages:\n        start, end = end, end + page.len\n        if is_reversed:\n            if start > scan_item.start:\n                break\n            if end < scan_item.stop:\n                continue\n        else:\n            if start > scan_item.stop:\n                break\n            if end < scan_item.start:\n                continue\n        ro = intercept(range(start, end), range_item)\n        if len(ro) == 0:\n            continue\n        elif len(ro) == page.len:  # share the whole immutable page\n            pages.append(page)\n        else:  # fetch the slice and filter it.\n            search_slice = slice(ro.start - start, ro.stop - start, ro.step)\n            np_arr = load_numpy(page.path)\n            match = np_arr[search_slice]\n            pages.append(match)\n\n    if is_reversed:\n        pages.reverse()\n        for ix, page in enumerate(pages):\n            if isinstance(page, SimplePage):\n                data = page.get()\n                pages[ix] = np.flip(data)\n            else:\n                pages[ix] = np.flip(page)\n\n    return pages\n
"},{"location":"reference/base/#tablite.base.Column.iter_by_page","title":"tablite.base.Column.iter_by_page()","text":"

iterates over the column, page by page. This method minimizes the number of reads.

RETURNS DESCRIPTION

generator of tuple: start: int end: int data: np.ndarray

Source code in tablite/base.py
def iter_by_page(self):\n    \"\"\"iterates over the column, page by page.\n    This method minimizes the number of reads.\n\n    Returns:\n        generator of tuple:\n            start: int\n            end: int\n            data: np.ndarray\n    \"\"\"\n    start, end = 0, 0\n    for page in self.pages:\n        start, end = end, end + page.len\n        yield start, end, page.get()\n
"},{"location":"reference/base/#tablite.base.Column.__getitem__","title":"tablite.base.Column.__getitem__(item)","text":"

gets numpy array.

PARAMETER DESCRIPTION item

slice of column

TYPE: int OR slice

RETURNS DESCRIPTION

np.ndarray: results as numpy array.

Remember:

>>> R = np.array([0,1,2,3,4,5])\n>>> R[3]\n3\n>>> R[3:4]\narray([3])\n
Source code in tablite/base.py
def __getitem__(self, item):  # USER FUNCTION.\n    \"\"\"gets numpy array.\n\n    Args:\n        item (int OR slice): slice of column\n\n    Returns:\n        np.ndarray: results as numpy array.\n\n    Remember:\n    ```\n    >>> R = np.array([0,1,2,3,4,5])\n    >>> R[3]\n    3\n    >>> R[3:4]\n    array([3])\n    ```\n    \"\"\"\n    result = []\n    for element in self.getpages(item):\n        if isinstance(element, SimplePage):\n            result.append(element.get())\n        else:\n            result.append(element)\n\n    if result:\n        arr = np_type_unify(result)\n    else:\n        arr = np.array([])\n\n    if isinstance(item, int):\n        if len(arr) == 0:\n            raise IndexError(\n                f\"index {item} is out of bounds for axis 0 with size {len(self)}\"\n            )\n        return numpy_to_python(arr[0])\n    else:\n        return arr\n
"},{"location":"reference/base/#tablite.base.Column.__setitem__","title":"tablite.base.Column.__setitem__(key, value)","text":"

sets values.

PARAMETER DESCRIPTION key

selector

TYPE: (int, slice)

value

values to insert

TYPE: any

RAISES DESCRIPTION KeyError

Following normal slicing rules

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION.\n    \"\"\"sets values.\n\n    Args:\n        key (int,slice): selector\n        value (any): values to insert\n\n    Raises:\n        KeyError: Following normal slicing rules\n    \"\"\"\n    if isinstance(key, int):\n        self._setitem_integer_key(key, value)\n\n    elif isinstance(key, slice):\n        if not isinstance(value, np.ndarray):\n            value = list_to_np_array(value)\n        type_check(value, np.ndarray)\n\n        if key.start is None and key.stop is None and key.step in (None, 1):\n            self._setitem_replace_all(key, value)\n        elif key.start is not None and key.stop is None and key.step in (None, 1):\n            self._setitem_extend(key, value)\n        elif key.stop is not None and key.start is None and key.step in (None, 1):\n            self._setitem_prextend(key, value)\n        elif (\n            key.step in (None, 1) and key.start is not None and key.stop is not None\n        ):\n            self._setitem_insert(key, value)\n        elif key.step not in (None, 1):\n            self._setitem_update(key, value)\n        else:\n            raise KeyError(f\"bad key: {key}\")\n    else:\n        raise KeyError(f\"bad key: {key}\")\n
"},{"location":"reference/base/#tablite.base.Column.__delitem__","title":"tablite.base.Column.__delitem__(key)","text":"

deletes items selected by key

PARAMETER DESCRIPTION key

selector

TYPE: (int, slice)

RAISES DESCRIPTION KeyError

following normal slicing rules.

Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION\n    \"\"\"deletes items selected by key\n\n    Args:\n        key (int,slice): selector\n\n    Raises:\n        KeyError: following normal slicing rules.\n    \"\"\"\n    if isinstance(key, int):\n        self._del_by_int(key)\n    elif isinstance(key, slice):\n        self._del_by_slice(key)\n    else:\n        raise KeyError(f\"bad key: {key}\")\n
"},{"location":"reference/base/#tablite.base.Column.get_by_indices","title":"tablite.base.Column.get_by_indices(indices)","text":"

retrieves values from column given a set of indices.

PARAMETER DESCRIPTION indices

targets

TYPE: array

This method uses np.take, is faster than iterating over rows. Examples:

>>> indices = np.array(list(range(3,700_700, 426)))\n>>> arr = np.array(list(range(2_000_000)))\nPythonic:\n>>> [v for i,v in enumerate(arr) if i in indices]\nNumpyionic:\n>>> np.take(arr, indices)\n
Source code in tablite/base.py
def get_by_indices(self, indices):\n    \"\"\"retrieves values from column given a set of indices.\n\n    Args:\n        indices (np.array): targets\n\n    This method uses np.take, is faster than iterating over rows.\n    Examples:\n    ```\n    >>> indices = np.array(list(range(3,700_700, 426)))\n    >>> arr = np.array(list(range(2_000_000)))\n    Pythonic:\n    >>> [v for i,v in enumerate(arr) if i in indices]\n    Numpyionic:\n    >>> np.take(arr, indices)\n    ```\n    \"\"\"\n    type_check(indices, np.ndarray)\n\n    dtypes = set()\n    values = np.empty(\n        indices.shape, dtype=object\n    )  # placeholder for the indexed values.\n\n    for start, end, data in self.iter_by_page():\n        range_match = np.asarray(\n            ((indices >= start) & (indices < end)) | (indices == -1)\n        ).nonzero()[0]\n        if len(range_match):\n            sub_index = np.take(indices, range_match)\n            sub_index2 = np.where(sub_index == -1, -1, sub_index - start)\n            # diss: the line above is required to cover for cases where len(data) > (-1 - start)\n            #       as sub_index2 otherwise will raise index error\n            arr = np.take(data, sub_index2)\n            dtypes.add(arr.dtype)\n            np.put(values, range_match, arr)\n\n    if len(dtypes) == 1:  # simplify the datatype.\n        dtype = next(iter(dtypes))\n        values = np.array(values, dtype=dtype)\n    return values\n
"},{"location":"reference/base/#tablite.base.Column.__iter__","title":"tablite.base.Column.__iter__()","text":"Source code in tablite/base.py
def __iter__(self):  # USER FUNCTION.\n    for page in self.pages:\n        data = page.get()\n        for value in data:\n            yield value\n
"},{"location":"reference/base/#tablite.base.Column.__eq__","title":"tablite.base.Column.__eq__(other)","text":"

compares two columns. Like list1 == list2

Source code in tablite/base.py
def __eq__(self, other):  # USER FUNCTION.\n    \"\"\"\n    compares two columns. Like `list1 == list2`\n    \"\"\"\n    if len(self) != len(other):  # quick cheap check.\n        return False\n\n    if isinstance(other, (list, tuple)):\n        return all(a == b for a, b in zip(self[:], other))\n\n    elif isinstance(other, Column):\n        if self.pages == other.pages:  # special case.\n            return True\n\n        # are the pages of same size?\n        if len(self.pages) == len(other.pages):\n            if [p.len for p in self.pages] == [p.len for p in other.pages]:\n                for a, b in zip(self.pages, other.pages):\n                    if not (a.get() == b.get()).all():\n                        return False\n                return True\n        # to bad. Element comparison it is then:\n        for a, b in zip(iter(self), iter(other)):\n            if a != b:\n                return False\n        return True\n\n    elif isinstance(other, np.ndarray):\n        start, end = 0, 0\n        for p in self.pages:\n            start, end = end, end + p.len\n            if not (p.get() == other[start:end]).all():\n                return False\n        return True\n    else:\n        raise TypeError(f\"Cannot compare {self.__class__} with {type(other)}\")\n
"},{"location":"reference/base/#tablite.base.Column.__ne__","title":"tablite.base.Column.__ne__(other)","text":"

compares two columns. Like list1 != list2

Source code in tablite/base.py
def __ne__(self, other):  # USER FUNCTION\n    \"\"\"\n    compares two columns. Like `list1 != list2`\n    \"\"\"\n    if len(self) != len(other):  # quick cheap check.\n        return True\n\n    if isinstance(other, (list, tuple)):\n        return any(a != b for a, b in zip(self[:], other))\n\n    elif isinstance(other, Column):\n        if self.pages == other.pages:  # special case.\n            return False\n\n        # are the pages of same size?\n        if len(self.pages) == len(other.pages):\n            if [p.len for p in self.pages] == [p.len for p in other.pages]:\n                for a, b in zip(self.pages, other.pages):\n                    if not (a.get() == b.get()).all():\n                        return True\n                return False\n        # to bad. Element comparison it is then:\n        for a, b in zip(iter(self), iter(other)):\n            if a != b:\n                return True\n        return False\n\n    elif isinstance(other, np.ndarray):\n        start, end = 0, 0\n        for p in self.pages:\n            start, end = end, end + p.len\n            if (p.get() != other[start:end]).any():\n                return True\n        return False\n    else:\n        raise TypeError(f\"Cannot compare {self.__class__} with {type(other)}\")\n
"},{"location":"reference/base/#tablite.base.Column.copy","title":"tablite.base.Column.copy()","text":"

returns deep=copy of Column

RETURNS DESCRIPTION

Column

Source code in tablite/base.py
def copy(self):\n    \"\"\"returns deep=copy of Column\n\n    Returns:\n        Column\n    \"\"\"\n    cp = Column(path=self.path)\n    cp.pages = self.pages[:]\n    return cp\n
"},{"location":"reference/base/#tablite.base.Column.__copy__","title":"tablite.base.Column.__copy__()","text":"

see copy

Source code in tablite/base.py
def __copy__(self):\n    \"\"\"see copy\"\"\"\n    return self.copy()\n
"},{"location":"reference/base/#tablite.base.Column.__imul__","title":"tablite.base.Column.__imul__(other)","text":"

Repeats instance of column N times. Like list() * N

Example:

>>> one = Column(data=[1,2])\n>>> one *= 5\n>>> one\n[1,2, 1,2, 1,2, 1,2, 1,2]\n
Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"\n    Repeats instance of column N times. Like list() * N\n\n    Example:\n    ```\n    >>> one = Column(data=[1,2])\n    >>> one *= 5\n    >>> one\n    [1,2, 1,2, 1,2, 1,2, 1,2]\n    ```\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a column can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    self.pages = self.pages[:] * other\n    return self\n
"},{"location":"reference/base/#tablite.base.Column.__mul__","title":"tablite.base.Column.__mul__(other)","text":"

Repeats instance of column N times. Like list() * N

Example:

>>> one = Column(data=[1,2])\n>>> two = one * 5\n>>> two\n[1,2, 1,2, 1,2, 1,2, 1,2]\n
Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"\n    Repeats instance of column N times. Like list() * N\n\n    Example:\n    ```\n    >>> one = Column(data=[1,2])\n    >>> two = one * 5\n    >>> two\n    [1,2, 1,2, 1,2, 1,2, 1,2]\n    ```\n    \"\"\"\n    if not isinstance(other, int):\n        raise TypeError(\n            f\"a column can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    cp = self.copy()\n    cp *= other\n    return cp\n
"},{"location":"reference/base/#tablite.base.Column.__iadd__","title":"tablite.base.Column.__iadd__(other)","text":"Source code in tablite/base.py
def __iadd__(self, other):\n    if isinstance(other, (list, tuple)):\n        other = list_to_np_array(other)\n        self.extend(other)\n    elif isinstance(other, Column):\n        self.pages.extend(other.pages[:])\n    else:\n        raise TypeError(f\"{type(other)} not supported.\")\n    return self\n
"},{"location":"reference/base/#tablite.base.Column.__contains__","title":"tablite.base.Column.__contains__(item)","text":"

determines if item is in the Column. Similar to 'x' in ['a','b','c'] returns boolean

PARAMETER DESCRIPTION item

value to search for

TYPE: any

RETURNS DESCRIPTION bool

True if item exists in column.

Source code in tablite/base.py
def __contains__(self, item):\n    \"\"\"determines if item is in the Column.\n    Similar to `'x' in ['a','b','c']`\n    returns boolean\n\n    Args:\n        item (any): value to search for\n\n    Returns:\n        bool: True if item exists in column.\n    \"\"\"\n    for page in set(self.pages):\n        if item in page.get():  # x in np.ndarray([...]) uses np.any(arr, value)\n            return True\n    return False\n
"},{"location":"reference/base/#tablite.base.Column.remove_all","title":"tablite.base.Column.remove_all(*values)","text":"

removes all values of values

Source code in tablite/base.py
def remove_all(self, *values):\n    \"\"\"\n    removes all values of `values`\n    \"\"\"\n    type_check(values, tuple)\n    if isinstance(values[0], tuple):\n        values = values[0]\n    to_remove = list_to_np_array(values)\n    for index, page in enumerate(self.pages):\n        data = page.get()\n        bitmask = np.isin(data, to_remove)  # identify elements to remove.\n        if bitmask.any():\n            bitmask = np.invert(bitmask)  # turn bitmask around to keep.\n            new_data = np.compress(bitmask, data)\n            new_page = Page(self.path, new_data)\n            self.pages[index] = new_page\n
"},{"location":"reference/base/#tablite.base.Column.replace","title":"tablite.base.Column.replace(mapping)","text":"

replaces values using a mapping.

PARAMETER DESCRIPTION mapping

{value to replace: new value, ...}

TYPE: dict

Example:

>>> t = Table(columns={'A': [1,2,3,4]})\n>>> t['A'].replace({2:20,4:40})\n>>> t[:]\nnp.ndarray([1,20,3,40])\n
Source code in tablite/base.py
def replace(self, mapping):\n    \"\"\"\n    replaces values using a mapping.\n\n    Args:\n        mapping (dict): {value to replace: new value, ...}\n\n    Example:\n    ```\n    >>> t = Table(columns={'A': [1,2,3,4]})\n    >>> t['A'].replace({2:20,4:40})\n    >>> t[:]\n    np.ndarray([1,20,3,40])\n    ```\n    \"\"\"\n    type_check(mapping, dict)\n    to_replace = np.array(list(mapping.keys()))\n    for index, page in enumerate(self.pages):\n        data = page.get()\n        bitmask = np.isin(data, to_replace)  # identify elements to replace.\n        if bitmask.any():\n            warray = np.compress(bitmask, data)\n            for ix, v in enumerate(warray):\n                warray[ix] = mapping[numpy_to_python(v)]\n            data[bitmask] = warray\n            self.pages[index] = Page(path=self.path, array=data)\n
"},{"location":"reference/base/#tablite.base.Column.types","title":"tablite.base.Column.types()","text":"

returns dict with python datatypes

RETURNS DESCRIPTION dict

frequency of occurrence of python datatypes

Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns dict with python datatypes\n\n    Returns:\n        dict: frequency of occurrence of python datatypes\n    \"\"\"\n    d = Counter()\n    for page in self.pages:\n        assert isinstance(page.dtype, dict)\n        d += page.dtype\n    return dict(d)\n
"},{"location":"reference/base/#tablite.base.Column.index","title":"tablite.base.Column.index()","text":"

returns dict with { unique entry : list of indices }

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.index()\n{'a':[0,2], 'b': [1,4], 'c': [3]}\n
Source code in tablite/base.py
def index(self):\n    \"\"\"\n    returns dict with { unique entry : list of indices }\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.index()\n    {'a':[0,2], 'b': [1,4], 'c': [3]}\n    ```\n    \"\"\"\n    d = defaultdict(list)\n    for ix, v in enumerate(self.__iter__()):\n        d[v].append(ix)\n    return dict(d)\n
"},{"location":"reference/base/#tablite.base.Column.unique","title":"tablite.base.Column.unique()","text":"

returns unique list of values.

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.unqiue()\n['a','b','c']\n
Source code in tablite/base.py
def unique(self):\n    \"\"\"\n    returns unique list of values.\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.unqiue()\n    ['a','b','c']\n    ```\n    \"\"\"\n    arrays = []\n    for page in set(self.pages):\n        try:  # when it works, numpy is fast...\n            arrays.append(np.unique(page.get()))\n        except TypeError:  # ...but np.unique cannot handle Nones.\n            arrays.append(multitype_set(page.get()))\n    union = np_type_unify(arrays)\n    try:\n        return np.unique(union)\n    except MemoryError:\n        return np.array(set(union))\n    except TypeError:\n        return multitype_set(union)\n
"},{"location":"reference/base/#tablite.base.Column.histogram","title":"tablite.base.Column.histogram()","text":"

returns 2 arrays: unique elements and count of each element

example:

>>> c = Column(data=['a','b','a','c','b'])\n>>> c.histogram()\n{'a':2,'b':2,'c':1}\n
Source code in tablite/base.py
def histogram(self):\n    \"\"\"\n    returns 2 arrays: unique elements and count of each element\n\n    example:\n    ```\n    >>> c = Column(data=['a','b','a','c','b'])\n    >>> c.histogram()\n    {'a':2,'b':2,'c':1}\n    ```\n    \"\"\"\n    d = defaultdict(int)\n    for page in self.pages:\n        try:\n            uarray, carray = np.unique(page.get(), return_counts=True)\n        except TypeError:\n            uarray = page.get()\n            carray = repeat(1, len(uarray))\n\n        for i, c in zip(uarray, carray):\n            v = numpy_to_python(i)\n            d[(type(v), v)] += numpy_to_python(c)\n    u = [v for _, v in d.keys()]\n    c = list(d.values())\n    return u, c  # unique, counts\n
"},{"location":"reference/base/#tablite.base.Column.statistics","title":"tablite.base.Column.statistics()","text":"

provides summary statistics.

RETURNS DESCRIPTION dict

returns dict with:

  • min (int/float, length of str, date)
  • max (int/float, length of str, date)
  • mean (int/float, length of str, date)
  • median (int/float, length of str, date)
  • stdev (int/float, length of str, date)
  • mode (int/float, length of str, date)
  • distinct (int/float, length of str, date)
  • iqr (int/float, length of str, date)
  • sum (int/float, length of str, date)
  • histogram (see .histogram)
Source code in tablite/base.py
def statistics(self):\n    \"\"\"provides summary statistics.\n\n    Returns:\n        dict: returns dict with:\n        - min (int/float, length of str, date)\n        - max (int/float, length of str, date)\n        - mean (int/float, length of str, date)\n        - median (int/float, length of str, date)\n        - stdev (int/float, length of str, date)\n        - mode (int/float, length of str, date)\n        - distinct (int/float, length of str, date)\n        - iqr (int/float, length of str, date)\n        - sum (int/float, length of str, date)\n        - histogram (see .histogram)\n    \"\"\"\n    values, counts = self.histogram()\n    return summary_statistics(values, counts)\n
"},{"location":"reference/base/#tablite.base.Column.count","title":"tablite.base.Column.count(item)","text":"

counts appearances of item in column.

Note that in python, True == 1 and False == 0, whereby the following difference occurs:

in python:

>>> L = [1, True]\n>>> L.count(True)\n2\n

in tablite:

>>> t = Table({'L': [1,True]})\n>>> t['L'].count(True)\n1\n
PARAMETER DESCRIPTION item

target item

TYPE: Any

RETURNS DESCRIPTION int

number of occurrences of item.

Source code in tablite/base.py
def count(self, item):\n    \"\"\"counts appearances of item in column.\n\n    Note that in python, `True == 1` and `False == 0`,\n    whereby the following difference occurs:\n\n    in python:\n    ```\n    >>> L = [1, True]\n    >>> L.count(True)\n    2\n    ```\n    in tablite:\n    ```\n    >>> t = Table({'L': [1,True]})\n    >>> t['L'].count(True)\n    1\n    ```\n\n    Args:\n        item (Any): target item\n\n    Returns:\n        int: number of occurrences of item.\n    \"\"\"\n    result = 0\n    for page in self.pages:\n        data = page.get()\n        if data.dtype != \"O\":\n            result += np.nonzero(page.get() == item)[0].shape[0]\n            # what happens here ---^ below:\n            # arr = page.get()\n            # >>> arr\n            # array([1,2,3,4,3], int64)\n            # >>> (arr == 3)\n            # array([False, False,  True, False,  True])\n            # >>> np.nonzero(arr==3)\n            # (array([2,4], dtype=int64), )  <-- tuple!\n            # >>> np.nonzero(page.get() == item)[0]\n            # array([2,4])\n            # >>> np.nonzero(page.get() == item)[0].shape\n            # (2, )\n            # >>> np.nonzero(page.get() == item)[0].shape[0]\n            # 2\n        else:\n            result += sum(1 for i in data if type(i) == type(item) and i == item)\n    return result\n
"},{"location":"reference/base/#tablite.base.Table","title":"tablite.base.Table(columns=None, headers=None, rows=None, _path=None)","text":"

Bases: object

creates Table

PARAMETER DESCRIPTION EITHER

columns (dict, optional): dict with column names as keys, values as lists. Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})

Source code in tablite/base.py
def __init__(self, columns=None, headers=None, rows=None, _path=None) -> None:\n    \"\"\"creates Table\n\n    Args:\n        EITHER:\n            columns (dict, optional): dict with column names as keys, values as lists.\n            Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})\n        OR\n            headers (list of strings, optional): list of column names.\n            rows (list of tuples or lists, optional): values for columns\n            Example: t = Table(headers=[\"a\", \"b\"], rows=[[1,3], [2,4]])\n    \"\"\"\n    if _path is None:\n        if self._pid_dir is None:\n            self._pid_dir = Path(Config.workdir) / Config.pid\n            if not self._pid_dir.exists():\n                self._pid_dir.mkdir()\n                (self._pid_dir / \"pages\").mkdir()\n            register(self._pid_dir)\n\n        _path = Path(self._pid_dir)\n        # if path exists under the given PID it will be overwritten.\n        # this can only happen if the process previously was SIGKILLed.\n    type_check(_path, Path)\n    self.path = _path  # filename used during multiprocessing.\n    self.columns = {}  # maps colunn names to instances of Column.\n\n    # user friendly features.\n    if columns and any((headers, rows)):\n        raise ValueError(\"Either columns as dict OR headers and rows. Not both.\")\n\n    if headers and rows:\n        rotated = list(zip(*rows))\n        columns = {k: v for k, v in zip(headers, rotated)}\n\n    if columns:\n        type_check(columns, dict)\n        for k, v in columns.items():\n            self.__setitem__(k, v)\n
"},{"location":"reference/base/#tablite.base.Table-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.Table.path","title":"tablite.base.Table.path = _path instance-attribute","text":""},{"location":"reference/base/#tablite.base.Table.columns","title":"tablite.base.Table.columns = {} instance-attribute","text":""},{"location":"reference/base/#tablite.base.Table.rows","title":"tablite.base.Table.rows property","text":"

enables row based iteration in python types.

Example:

for row in Table.rows:\n    print(row)\n

Yields: tuple: values is same order as columns.

"},{"location":"reference/base/#tablite.base.Table-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.Table.__str__","title":"tablite.base.Table.__str__()","text":"Source code in tablite/base.py
def __str__(self):  # USER FUNCTION.\n    return f\"{self.__class__.__name__}({len(self.columns):,} columns, {len(self):,} rows)\"\n
"},{"location":"reference/base/#tablite.base.Table.__repr__","title":"tablite.base.Table.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return self.__str__()\n
"},{"location":"reference/base/#tablite.base.Table.nbytes","title":"tablite.base.Table.nbytes()","text":"

finds the total bytes of the table on disk

RETURNS DESCRIPTION tuple

int: real bytes used on disk int: total bytes used if flattened

Source code in tablite/base.py
def nbytes(self):  # USER FUNCTION.\n    \"\"\"finds the total bytes of the table on disk\n\n    Returns:\n        tuple:\n            int: real bytes used on disk\n            int: total bytes used if flattened\n    \"\"\"\n    real = {}\n    total = 0\n    for column in self.columns.values():\n        for page in set(column.pages):\n            real[page] = page.path.stat().st_size\n        for page in column.pages:\n            total += real[page]\n    return sum(real.values()), total\n
"},{"location":"reference/base/#tablite.base.Table.items","title":"tablite.base.Table.items()","text":"

returns table as dict

RETURNS DESCRIPTION dict

Table as dict {column_name: [values], ...}

Source code in tablite/base.py
def items(self):  # USER FUNCTION.\n    \"\"\"returns table as dict\n\n    Returns:\n        dict: Table as dict `{column_name: [values], ...}`\n    \"\"\"\n    return {\n        name: column[:].tolist() for name, column in self.columns.items()\n    }.items()\n
"},{"location":"reference/base/#tablite.base.Table.__delitem__","title":"tablite.base.Table.__delitem__(key)","text":"

Examples:

>>> del table['a']  # removes column 'a'\n>>> del table[-3:]  # removes last 3 rows from all columns.\n
Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION.\n    \"\"\"\n    Examples:\n    ```\n    >>> del table['a']  # removes column 'a'\n    >>> del table[-3:]  # removes last 3 rows from all columns.\n    ```\n    \"\"\"\n    if isinstance(key, (int, slice)):\n        for column in self.columns.values():\n            del column[key]\n    elif key in self.columns:\n        del self.columns[key]\n    else:\n        raise KeyError(f\"Key not found: {key}\")\n
"},{"location":"reference/base/#tablite.base.Table.__setitem__","title":"tablite.base.Table.__setitem__(key, value)","text":"

table behaves like a dict. Args: key (str or hashable): column name value (iterable): list, tuple or nd.array with values.

As Table now accepts the keyword columns as a dict:

>>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n

and the header/data combinations:

>>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n

This has the side-benefit that tuples now can be used as headers.

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION\n    \"\"\"table behaves like a dict.\n    Args:\n        key (str or hashable): column name\n        value (iterable): list, tuple or nd.array with values.\n\n    As Table now accepts the keyword `columns` as a dict:\n    ```\n    >>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n    ```\n    and the header/data combinations:\n    ```\n    >>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n    ```\n    This has the side-benefit that tuples now can be used as headers.\n    \"\"\"\n    if value is None:\n        self.columns[key] = Column(self.path, value=None)\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, (np.ndarray)):\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, Column):\n        self.columns[key] = value\n    else:\n        raise TypeError(f\"{type(value)} not supported.\")\n
"},{"location":"reference/base/#tablite.base.Table.__getitem__","title":"tablite.base.Table.__getitem__(keys)","text":"

Enables selection of columns and rows

PARAMETER DESCRIPTION keys

TYPE: column name, integer or slice

Examples

>>>

10] selects first 10 rows from all columns

TYPE: table[

>>>

20:3] selects column 'b' and 'c' and 'a' twice for a slice.

TYPE: table['b', 'a', 'a', 'c', 2

Raises: KeyError: if key is not found. TypeError: if key is not a string, integer or slice.

RETURNS DESCRIPTION Table

returns columns in same order as selection.

Source code in tablite/base.py
def __getitem__(self, keys):  # USER FUNCTION\n    \"\"\"\n    Enables selection of columns and rows\n\n    Args:\n        keys (column name, integer or slice):\n        Examples:\n        ```\n        >>> table['a']                        selects column 'a'\n        >>> table[3]                          selects row 3 as a tuple.\n        >>> table[:10]                        selects first 10 rows from all columns\n        >>> table['a','b', slice(3,20,2)]     selects a slice from columns 'a' and 'b'\n        >>> table['b', 'a', 'a', 'c', 2:20:3] selects column 'b' and 'c' and 'a' twice for a slice.\n        >>> table[('b', 'a', 'a', 'c')]       selects columns 'b', 'a', 'a', and 'c' using a tuple.\n        ```\n    Raises:\n        KeyError: if key is not found.\n        TypeError: if key is not a string, integer or slice.\n\n    Returns:\n        Table: returns columns in same order as selection.\n    \"\"\"\n\n    if not isinstance(keys, tuple):\n        if isinstance(keys, list):\n            keys = tuple(keys)\n        else:\n            keys = (keys,)\n    if isinstance(keys[0], tuple):\n        keys = tuple(list(chain(*keys)))\n\n    integers = [i for i in keys if isinstance(i, int)]\n    if len(integers) == len(keys) == 1:  # return a single tuple.\n        keys = [slice(keys[0])]\n\n    column_names = [i for i in keys if isinstance(i, str)]\n    column_names = list(self.columns) if not column_names else column_names\n    not_found = [name for name in column_names if name not in self.columns]\n    if not_found:\n        raise KeyError(f\"keys not found: {', '.join(not_found)}\")\n\n    slices = [i for i in keys if isinstance(i, slice)]\n    slc = slice(0, len(self)) if not slices else slices[0]\n\n    if (\n        len(slices) == 0 and len(column_names) == 1\n    ):  # e.g. tbl['a'] or tbl['a'][:10]\n        col = self.columns[column_names[0]]\n        if slices:\n            return col[slc]  # return slice from column as list of values\n        else:\n            return col  # return whole column\n\n    elif len(integers) == 1:  # return a single tuple.\n        row_no = integers[0]\n        slc = slice(row_no, row_no + 1)\n        return tuple(self.columns[name][slc].tolist()[0] for name in column_names)\n\n    elif not slices:  # e.g. new table with N whole columns.\n        return self.__class__(\n            columns={name: self.columns[name] for name in column_names}\n        )\n\n    else:  # e.g. new table from selection of columns and slices.\n        t = self.__class__()\n        for name in column_names:\n            column = self.columns[name]\n\n            new_column = Column(t.path)  # create new Column.\n            for item in column.getpages(slc):\n                if isinstance(item, np.ndarray):\n                    new_column.extend(item)  # extend subslice (expensive)\n                elif isinstance(item, SimplePage):\n                    new_column.pages.append(item)  # extend page (cheap)\n                else:\n                    raise TypeError(f\"Bad item: {item}\")\n\n            # below:\n            # set the new column directly on t.columns.\n            # Do not use t[name] as that triggers __setitem__ again.\n            t.columns[name] = new_column\n\n        return t\n
"},{"location":"reference/base/#tablite.base.Table.__len__","title":"tablite.base.Table.__len__()","text":"Source code in tablite/base.py
def __len__(self):  # USER FUNCTION.\n    if not self.columns:\n        return 0\n    return max(len(c) for c in self.columns.values())\n
"},{"location":"reference/base/#tablite.base.Table.__eq__","title":"tablite.base.Table.__eq__(other) -> bool","text":"

Determines if two tables have identical content.

PARAMETER DESCRIPTION other

table for comparison

TYPE: Table

RETURNS DESCRIPTION bool

True if tables are identical.

TYPE: bool

Source code in tablite/base.py
def __eq__(self, other) -> bool:  # USER FUNCTION.\n    \"\"\"Determines if two tables have identical content.\n\n    Args:\n        other (Table): table for comparison\n\n    Returns:\n        bool: True if tables are identical.\n    \"\"\"\n    if isinstance(other, dict):\n        return self.items() == other.items()\n    if not isinstance(other, Table):\n        return False\n    if id(self) == id(other):\n        return True\n    if len(self) != len(other):\n        return False\n    if len(self) == len(other) == 0:\n        return True\n    if self.columns.keys() != other.columns.keys():\n        return False\n    for name, col in self.columns.items():\n        if not (col == other.columns[name]):\n            return False\n    return True\n
"},{"location":"reference/base/#tablite.base.Table.clear","title":"tablite.base.Table.clear()","text":"

clears the table. Like dict().clear()

Source code in tablite/base.py
def clear(self):  # USER FUNCTION.\n    \"\"\"clears the table. Like dict().clear()\"\"\"\n    self.columns.clear()\n
"},{"location":"reference/base/#tablite.base.Table.save","title":"tablite.base.Table.save(path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1)","text":"

saves table to compressed tpz file.

PARAMETER DESCRIPTION path

file destination.

TYPE: Path

compression_method

See zipfile compression methods. Defaults to ZIP_DEFLATED.

DEFAULT: ZIP_DEFLATED

compression_level

See zipfile compression levels. Defaults to 1.

DEFAULT: 1

The file format is as follows: .tpz is a gzip archive with table metadata captured as table.yml and the necessary set of pages saved as .npy files.

The zip contains table.yml which provides an overview of the data:

--------------------------------------\n%YAML 1.2                              yaml version\ncolumns:                               start of columns section.\n    name: \u201c\u5217 1\u201d                       name of column 1.\n        pages: [p1b1, p1b2]            list of pages in column 1.\n    name: \u201c\u5217 2\u201d                       name of column 2\n        pages: [p2b1, p2b2]            list of pages in column 2.\n----------------------------------------\n
Source code in tablite/base.py
def save(\n    self, path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1\n):  # USER FUNCTION.\n    \"\"\"saves table to compressed tpz file.\n\n    Args:\n        path (Path): file destination.\n        compression_method: See zipfile compression methods. Defaults to ZIP_DEFLATED.\n        compression_level: See zipfile compression levels. Defaults to 1.\n        The default settings produce 80% compression at 10% slowdown.\n\n    The file format is as follows:\n    .tpz is a gzip archive with table metadata captured as table.yml\n    and the necessary set of pages saved as .npy files.\n\n    The zip contains table.yml which provides an overview of the data:\n    ```\n    --------------------------------------\n    %YAML 1.2                              yaml version\n    columns:                               start of columns section.\n        name: \u201c\u5217 1\u201d                       name of column 1.\n            pages: [p1b1, p1b2]            list of pages in column 1.\n        name: \u201c\u5217 2\u201d                       name of column 2\n            pages: [p2b1, p2b2]            list of pages in column 2.\n    ----------------------------------------\n    ```\n    \"\"\"\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path += \".tpz\"\n\n    # create yaml document\n    _page_counter = 0\n    d = {}\n    cols = {}\n    for name, col in self.columns.items():\n        type_check(col, Column)\n        cols[name] = {\"pages\": [p.path.name for p in col.pages]}\n        _page_counter += len(col.pages)\n    d[\"columns\"] = cols\n    yml = yaml.safe_dump(\n        d, sort_keys=False, allow_unicode=True, default_flow_style=None\n    )\n\n    _file_counter = 0\n    with zipfile.ZipFile(\n        path, \"w\", compression=compression_method, compresslevel=compression_level\n    ) as f:\n        log.debug(f\"writing .tpz to {path} with\\n{yml}\")\n        f.writestr(\"table.yml\", yml)\n        for name, col in self.columns.items():\n            for page in set(\n                col.pages\n            ):  # set of pages! remember t *= 1000 repeats t 1000x\n                with open(page.path, \"rb\", buffering=0) as raw_io:\n                    f.writestr(page.path.name, raw_io.read())\n                _file_counter += 1\n                log.debug(f\"adding Page {page.path}\")\n\n        _fields = len(self) * len(self.columns)\n        _avg = _fields // _page_counter\n        log.debug(\n            f\"Wrote {_fields:,} on {_page_counter:,} pages in {_file_counter} files: {_avg} fields/page\"\n        )\n
"},{"location":"reference/base/#tablite.base.Table.load","title":"tablite.base.Table.load(path, tqdm=_tqdm) classmethod","text":"

loads a table from .tpz file. See also Table.save for details on the file format.

PARAMETER DESCRIPTION path

source file

TYPE: Path

RETURNS DESCRIPTION Table

table in read-only mode.

Source code in tablite/base.py
@classmethod\ndef load(cls, path, tqdm=_tqdm):  # USER FUNCTION.\n    \"\"\"loads a table from .tpz file.\n    See also Table.save for details on the file format.\n\n    Args:\n        path (Path): source file\n\n    Returns:\n        Table: table in read-only mode.\n    \"\"\"\n    type_check(path, Path)\n    log.debug(f\"loading {path}\")\n    with zipfile.ZipFile(path, \"r\") as f:\n        yml = f.read(\"table.yml\")\n        metadata = yaml.safe_load(yml)\n        t = cls()\n\n        page_count = sum([len(c[\"pages\"]) for c in metadata[\"columns\"].values()])\n\n        with tqdm(\n            total=page_count,\n            desc=f\"loading '{path.name}' file\",\n            disable=Config.TQDM_DISABLE,\n        ) as pbar:\n            for name, d in metadata[\"columns\"].items():\n                column = Column(t.path)\n                for page in d[\"pages\"]:\n                    bytestream = io.BytesIO(f.read(page))\n                    data = np.load(bytestream, allow_pickle=True, fix_imports=False)\n                    column.extend(data)\n                    pbar.update(1)\n                t.columns[name] = column\n    update_access_time(path)\n    return t\n
"},{"location":"reference/base/#tablite.base.Table.copy","title":"tablite.base.Table.copy()","text":"Source code in tablite/base.py
def copy(self):\n    cls = type(self)\n    t = cls()\n    for name, column in self.columns.items():\n        new = Column(t.path)\n        new.pages = column.pages[:]\n        t.columns[name] = new\n    return t\n
"},{"location":"reference/base/#tablite.base.Table.__imul__","title":"tablite.base.Table.__imul__(other)","text":"

Repeats instance of table N times.

Like list: t = t * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"Repeats instance of table N times.\n\n    Like list: `t = t * N`\n\n    Args:\n        other (int): multiplier\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a table can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    for col in self.columns.values():\n        col *= other\n    return self\n
"},{"location":"reference/base/#tablite.base.Table.__mul__","title":"tablite.base.Table.__mul__(other)","text":"

Repeat table N times. Like list: new = old * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"Repeat table N times.\n    Like list: `new = old * N`\n\n    Args:\n        other (int): multiplier\n\n    Returns:\n        Table\n    \"\"\"\n    new = self.copy()\n    return new.__imul__(other)\n
"},{"location":"reference/base/#tablite.base.Table.__iadd__","title":"tablite.base.Table.__iadd__(other)","text":"

Concatenates tables with same column names.

Like list: table_1 += table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION None

self is updated.

Source code in tablite/base.py
def __iadd__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_1 += table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        None: self is updated.\n    \"\"\"\n    type_check(other, Table)\n    for name in self.columns.keys():\n        if name not in other.columns:\n            raise ValueError(f\"{name} not in other\")\n    for name in other.columns.keys():\n        if name not in self.columns:\n            raise ValueError(f\"{name} missing from self\")\n\n    for name, column in self.columns.items():\n        other_col = other.columns.get(name, None)\n        column.pages.extend(other_col.pages[:])\n    return self\n
"},{"location":"reference/base/#tablite.base.Table.__add__","title":"tablite.base.Table.__add__(other)","text":"

Concatenates tables with same column names.

Like list: table_3 = table_1 + table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __add__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_3 = table_1 + table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        Table\n    \"\"\"\n    type_check(other, Table)\n    cp = self.copy()\n    cp += other\n    return cp\n
"},{"location":"reference/base/#tablite.base.Table.add_rows","title":"tablite.base.Table.add_rows(*args, **kwargs)","text":"

its more efficient to add many rows at once.

if both args and kwargs, then args are added first, followed by kwargs.

supported cases:

>>> t = Table()\n>>> t.add_columns('row','A','B','C')\n>>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n>>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n>>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n>>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n>>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n>>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n>>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n>>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n>>> t.add_rows(\n    {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n    )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n>>> t.add_rows( *[\n    {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n    ])                                                  # (10) list of dicts as args\n>>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n
Source code in tablite/base.py
def add_rows(self, *args, **kwargs):\n    \"\"\"its more efficient to add many rows at once.\n\n    if both args and kwargs, then args are added first, followed by kwargs.\n\n    supported cases:\n    ```\n    >>> t = Table()\n    >>> t.add_columns('row','A','B','C')\n    >>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n    >>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n    >>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n    >>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n    >>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n    >>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n    >>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n    >>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n    >>> t.add_rows(\n        {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n        )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n    >>> t.add_rows( *[\n        {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n        ])                                                  # (10) list of dicts as args\n    >>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n    ```\n\n    \"\"\"\n    if not Table._add_row_slow_warning:\n        warnings.warn(\n            \"add_rows is slow. Consider using add_columns and then assigning values to the columns directly.\"\n        )\n        Table._add_row_slow_warning = True\n\n    if args:\n        if not all(isinstance(i, (list, tuple, dict)) for i in args):  # 1,4\n            args = [args]\n\n        if all(isinstance(i, (list, tuple, dict)) for i in args):  # 2,3,7,8\n            # 1. turn the data into columns:\n\n            d = {n: [] for n in self.columns}\n            for arg in args:\n                if len(arg) != len(self.columns):\n                    raise ValueError(\n                        f\"len({arg})== {len(arg)}, but there are {len(self.columns)} columns\"\n                    )\n\n                if isinstance(arg, dict):\n                    for k, v in arg.items():  # 7,8\n                        d[k].append(v)\n\n                elif isinstance(arg, (list, tuple)):  # 2,3\n                    for n, v in zip(self.columns, arg):\n                        d[n].append(v)\n\n                else:\n                    raise TypeError(f\"{arg}?\")\n            # 2. extend the columns\n            for n, values in d.items():\n                col = self.columns[n]\n                col.extend(list_to_np_array(values))\n\n    if kwargs:\n        if isinstance(kwargs, dict):\n            if all(isinstance(v, (list, tuple)) for v in kwargs.values()):\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(list_to_np_array(v))\n            else:\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(np.array([v]))\n        else:\n            raise ValueError(f\"format not recognised: {kwargs}\")\n\n    return\n
"},{"location":"reference/base/#tablite.base.Table.add_columns","title":"tablite.base.Table.add_columns(*names)","text":"

Adds column names to table.

Source code in tablite/base.py
def add_columns(self, *names):\n    \"\"\"Adds column names to table.\"\"\"\n    for name in names:\n        self.columns[name] = Column(self.path)\n
"},{"location":"reference/base/#tablite.base.Table.add_column","title":"tablite.base.Table.add_column(name, data=None)","text":"

verbose alias for table[name] = data, that checks if name already exists

PARAMETER DESCRIPTION name

column name

TYPE: str

data

values. Defaults to None.

TYPE: list,tuple) DEFAULT: None

RAISES DESCRIPTION TypeError

name isn't string

ValueError

name already exists

Source code in tablite/base.py
def add_column(self, name, data=None):\n    \"\"\"verbose alias for table[name] = data, that checks if name already exists\n\n    Args:\n        name (str): column name\n        data ((list,tuple), optional): values. Defaults to None.\n\n    Raises:\n        TypeError: name isn't string\n        ValueError: name already exists\n    \"\"\"\n    if not isinstance(name, str):\n        raise TypeError(\"expected name as string\")\n    if name in self.columns:\n        raise ValueError(f\"{name} already in {self.columns}\")\n    self.__setitem__(name, data)\n
"},{"location":"reference/base/#tablite.base.Table.stack","title":"tablite.base.Table.stack(other)","text":"

returns the joint stack of tables with overlapping column names. Example:

| Table A|  +  | Table B| = |  Table AB |\n| A| B| C|     | A| B| D|   | A| B| C| -|\n                            | A| B| -| D|\n
Source code in tablite/base.py
def stack(self, other):\n    \"\"\"\n    returns the joint stack of tables with overlapping column names.\n    Example:\n    ```\n    | Table A|  +  | Table B| = |  Table AB |\n    | A| B| C|     | A| B| D|   | A| B| C| -|\n                                | A| B| -| D|\n    ```\n    \"\"\"\n    if not isinstance(other, Table):\n        raise TypeError(f\"stack only works for Table, not {type(other)}\")\n\n    cp = self.copy()\n    for name, col2 in other.columns.items():\n        if name not in cp.columns:\n            cp[name] = [None] * len(self)\n        cp[name].pages.extend(col2.pages[:])\n\n    for name in self.columns:\n        if name not in other.columns:\n            if len(cp) > 0:\n                cp[name].extend(np.array([None] * len(other)))\n    return cp\n
"},{"location":"reference/base/#tablite.base.Table.types","title":"tablite.base.Table.types()","text":"

returns nested dict of data types in the form: {column name: {python type class: number of instances }, ... }

example:

>>> t.types()\n{\n    'A': {<class 'str'>: 7},\n    'B': {<class 'int'>: 7}\n}\n
Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns nested dict of data types in the form:\n    `{column name: {python type class: number of instances }, ... }`\n\n    example:\n    ```\n    >>> t.types()\n    {\n        'A': {<class 'str'>: 7},\n        'B': {<class 'int'>: 7}\n    }\n    ```\n    \"\"\"\n    d = {}\n    for name, col in self.columns.items():\n        assert isinstance(col, Column)\n        d[name] = col.types()\n    return d\n
"},{"location":"reference/base/#tablite.base.Table.display_dict","title":"tablite.base.Table.display_dict(slice_=None, blanks=None, dtype=False)","text":"

helper for creating dict for display.

PARAMETER DESCRIPTION slice_

python slice. Defaults to None.

TYPE: slice DEFAULT: None

blanks

fill value for None. Defaults to None.

TYPE: optional DEFAULT: None

dtype

Adds datatype to each column. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION TypeError

slice_ must be None or slice.

RETURNS DESCRIPTION dict

from Table.

Source code in tablite/base.py
def display_dict(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"helper for creating dict for display.\n\n    Args:\n        slice_ (slice, optional): python slice. Defaults to None.\n        blanks (optional): fill value for `None`. Defaults to None.\n        dtype (bool, optional): Adds datatype to each column. Defaults to False.\n\n    Raises:\n        TypeError: slice_ must be None or slice.\n\n    Returns:\n        dict: from Table.\n    \"\"\"\n    if not self.columns:\n        print(\"Empty Table\")\n        return\n\n    def datatype(col):  # PRIVATE\n        \"\"\"creates label for column datatype.\"\"\"\n        types = col.types()\n        if len(types) == 1:\n            dt, _ = types.popitem()\n            typ = dt.__name__\n        else:\n            typ = \"mixed\"\n        return typ\n\n    row_count_tags = [\"#\", \"~\", \"*\"]\n    cols = set(self.columns)\n    for n, tag in product(range(1, 6), row_count_tags):\n        if n * tag not in cols:\n            tag = n * tag\n            break\n\n    if not isinstance(slice_, (slice, type(None))):\n        raise TypeError(f\"slice_ must be None or slice, not {type(slice_)}\")\n    if isinstance(slice_, slice):\n        slc = slice_\n    if slice_ is None:\n        if len(self) <= 20:\n            slc = slice(0, 20, 1)\n        else:\n            slc = None\n\n    n = len(self)\n    if slc:  # either we want slc or we want everything.\n        row_no = list(range(*slc.indices(len(self))))\n        data = {tag: [f\"{i:,}\".rjust(2) for i in row_no]}\n        for name, col in self.columns.items():\n            data[name] = list(chain(iter(col), repeat(blanks, times=n - len(col))))[\n                slc\n            ]\n    else:\n        data = {}\n        j = int(math.ceil(math.log10(n)) / 3) + len(str(n))\n        row_no = (\n            [f\"{i:,}\".rjust(j) for i in range(7)]\n            + [\"...\"]\n            + [f\"{i:,}\".rjust(j) for i in range(n - 7, n)]\n        )\n        data = {tag: row_no}\n\n        for name, col in self.columns.items():\n            if len(col) == n:\n                row = col[:7].tolist() + [\"...\"] + col[-7:].tolist()\n            else:\n                empty = [blanks] * 7\n                head = (col[:7].tolist() + empty)[:7]\n                tail = (col[n - 7 :].tolist() + empty)[-7:]\n                row = head + [\"...\"] + tail\n            data[name] = row\n\n    if dtype:\n        for name, values in data.items():\n            if name in self.columns:\n                col = self.columns[name]\n                values.insert(0, datatype(col))\n            else:\n                values.insert(0, \"row\")\n\n    return data\n
"},{"location":"reference/base/#tablite.base.Table.to_ascii","title":"tablite.base.Table.to_ascii(slice_=None, blanks=None, dtype=False)","text":"

returns ascii view of table as string.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def to_ascii(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"returns ascii view of table as string.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n\n    def adjust(v, length):  # PRIVATE FUNCTION\n        \"\"\"whitespace justifies field values based on datatype\"\"\"\n        if v is None:\n            return str(blanks).ljust(length)\n        elif isinstance(v, str):\n            return v.ljust(length)\n        else:\n            return str(v).rjust(length)\n\n    if not self.columns:\n        return str(self)\n\n    d = {}\n    for name, values in self.display_dict(\n        slice_=slice_, blanks=blanks, dtype=dtype\n    ).items():\n        as_text = [str(v) for v in values] + [str(name)]\n        width = max(len(i) for i in as_text)\n        new_name = name.center(width, \" \")\n        if dtype:\n            values[0] = values[0].center(width, \" \")\n        d[new_name] = [adjust(v, width) for v in values]\n\n    rows = dict_to_rows(d)\n    s = []\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n    s.append(\"|\" + \"|\".join(rows[0]) + \"|\")  # column names\n    start = 1\n    if dtype:\n        s.append(\"|\" + \"|\".join(rows[1]) + \"|\")  # datatypes\n        start = 2\n\n    s.append(\"+\" + \"+\".join([\"-\" * len(n) for n in rows[0]]) + \"+\")\n    for row in rows[start:]:\n        s.append(\"|\" + \"|\".join(row) + \"|\")\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n\n    if len(set(len(c) for c in self.columns.values())) != 1:\n        warning = f\"Warning: Columns have different lengths. {blanks} is used as fill value.\"\n        s.append(warning)\n\n    return \"\\n\".join(s)\n
"},{"location":"reference/base/#tablite.base.Table.show","title":"tablite.base.Table.show(slice_=None, blanks=None, dtype=False)","text":"

prints ascii view of table.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def show(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"prints ascii view of table.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n    print(self.to_ascii(slice_=slice_, blanks=blanks, dtype=dtype))\n
"},{"location":"reference/base/#tablite.base.Table.to_dict","title":"tablite.base.Table.to_dict(columns=None, slice_=None)","text":"

columns: list of column names. Default is None == all columns. slice_: slice. Default is None == all rows.

returns: dict with columns as keys and lists of values.

Example:

>>> t.show()\n+===+===+===+\n| # | a | b |\n|row|int|int|\n+---+---+---+\n| 0 |  1|  3|\n| 1 |  2|  4|\n+===+===+===+\n>>> t.to_dict()\n{'a':[1,2], 'b':[3,4]}\n
Source code in tablite/base.py
def to_dict(self, columns=None, slice_=None):\n    \"\"\"\n    columns: list of column names. Default is None == all columns.\n    slice_: slice. Default is None == all rows.\n\n    returns: dict with columns as keys and lists of values.\n\n    Example:\n    ```\n    >>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  3|\n    | 1 |  2|  4|\n    +===+===+===+\n    >>> t.to_dict()\n    {'a':[1,2], 'b':[3,4]}\n    ```\n\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n    assert isinstance(slice_, slice)\n\n    if columns is None:\n        columns = list(self.columns.keys())\n    if not isinstance(columns, list):\n        raise TypeError(\"expected columns as list of strings\")\n\n    return {name: list(self.columns[name][slice_]) for name in columns}\n
"},{"location":"reference/base/#tablite.base.Table.as_json_serializable","title":"tablite.base.Table.as_json_serializable(row_count='row id', start_on=1, columns=None, slice_=None)","text":"

provides a JSON compatible format of the table.

PARAMETER DESCRIPTION row_count

Label for row counts. Defaults to \"row id\".

TYPE: str DEFAULT: 'row id'

start_on

row counts starts by default on 1.

TYPE: int DEFAULT: 1

columns

Column names. Defaults to None which returns all columns.

TYPE: list of str DEFAULT: None

slice_

selector. Defaults to None which returns [:]

TYPE: slice DEFAULT: None

RETURNS DESCRIPTION

JSON serializable dict: All python datatypes have been converted to JSON compliant data.

Source code in tablite/base.py
def as_json_serializable(\n    self, row_count=\"row id\", start_on=1, columns=None, slice_=None\n):\n    \"\"\"provides a JSON compatible format of the table.\n\n    Args:\n        row_count (str, optional): Label for row counts. Defaults to \"row id\".\n        start_on (int, optional): row counts starts by default on 1.\n        columns (list of str, optional): Column names.\n            Defaults to None which returns all columns.\n        slice_ (slice, optional): selector. Defaults to None which returns [:]\n\n    Returns:\n        JSON serializable dict: All python datatypes have been converted to JSON compliant data.\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n\n    assert isinstance(slice_, slice)\n    new = {\"columns\": {}, \"total_rows\": len(self)}\n    if row_count is not None:\n        new[\"columns\"][row_count] = [\n            i + start_on for i in range(*slice_.indices(len(self)))\n        ]\n\n    d = self.to_dict(columns, slice_=slice_)\n    for k, data in d.items():\n        new_k = unique_name(\n            k, new[\"columns\"]\n        )  # used to avoid overwriting the `row id` key.\n        new[\"columns\"][new_k] = [\n            DataTypes.to_json(v) for v in data\n        ]  # deal with non-json datatypes.\n    return new\n
"},{"location":"reference/base/#tablite.base.Table.index","title":"tablite.base.Table.index(*args)","text":"

param: *args: column names returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}

Examples:

>>> table6 = Table()\n>>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n>>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n
>>> table6.index('A')  # single key.\n{('Alice',): [0],\n ('Bob',): [1, 2],\n ('Ben',): [3, 5],\n ('Charlie',): [4],\n ('Albert',): [6]})\n
>>> table6.index('A', 'B')  # multiple keys.\n{('Alice', 'Alison'): [0],\n ('Bob', 'Marley'): [1],\n ('Bob', 'Dylan'): [2],\n ('Ben', 'Affleck'): [3],\n ('Charlie', 'Hepburn'): [4],\n ('Ben', 'Barnes'): [5],\n ('Albert', 'Einstein'): [6]})\n
Source code in tablite/base.py
def index(self, *args):\n    \"\"\"\n    param: *args: column names\n    returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}\n\n    Examples:\n        ```\n        >>> table6 = Table()\n        >>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n        >>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n        ```\n\n        ```\n        >>> table6.index('A')  # single key.\n        {('Alice',): [0],\n         ('Bob',): [1, 2],\n         ('Ben',): [3, 5],\n         ('Charlie',): [4],\n         ('Albert',): [6]})\n        ```\n\n        ```\n        >>> table6.index('A', 'B')  # multiple keys.\n        {('Alice', 'Alison'): [0],\n         ('Bob', 'Marley'): [1],\n         ('Bob', 'Dylan'): [2],\n         ('Ben', 'Affleck'): [3],\n         ('Charlie', 'Hepburn'): [4],\n         ('Ben', 'Barnes'): [5],\n         ('Albert', 'Einstein'): [6]})\n        ```\n\n    \"\"\"\n    idx = defaultdict(list)\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in enumerate(zip(*iterators)):\n        key = tuple(numpy_to_python(k) for k in key)\n        idx[key].append(ix)\n    return idx\n
"},{"location":"reference/base/#tablite.base.Table.unique_index","title":"tablite.base.Table.unique_index(*args, tqdm=_tqdm)","text":"

generates the index of unique rows given a list of column names

PARAMETER DESCRIPTION *args

columns names

TYPE: any DEFAULT: ()

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION

np.array(int64): indices of unique records.

Source code in tablite/base.py
def unique_index(self, *args, tqdm=_tqdm):\n    \"\"\"generates the index of unique rows given a list of column names\n\n    Args:\n        *args (any): columns names\n        tqdm (tqdm, optional): Defaults to _tqdm.\n\n    Returns:\n        np.array(int64): indices of unique records.\n    \"\"\"\n    if not args:\n        raise ValueError(\"*args (column names) is required\")\n    seen = set()\n    unique = set()\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in tqdm(enumerate(zip(*iterators)), disable=Config.TQDM_DISABLE):\n        key_hash = hash(tuple(numpy_to_python(k) for k in key))\n        if key_hash in seen:\n            continue\n        else:\n            seen.add(key_hash)\n            unique.add(ix)\n    return np.array(sorted(unique))\n
"},{"location":"reference/base/#tablite.base-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.register","title":"tablite.base.register(path)","text":"

registers path in file_registry

The method is used by Table during init when the working directory path is set, so that python can clean all temporary files up at exit.

PARAMETER DESCRIPTION path

typically tmp/tablite-tmp/PID-{os.getpid()}

TYPE: Path

Source code in tablite/base.py
def register(path):\n    \"\"\"registers path in file_registry\n\n    The method is used by Table during init when the working directory path\n    is set, so that python can clean all temporary files up at exit.\n\n    Args:\n        path (Path): typically tmp/tablite-tmp/PID-{os.getpid()}\n    \"\"\"\n    global file_registry\n    file_registry.add(path)\n
"},{"location":"reference/base/#tablite.base.shutdown","title":"tablite.base.shutdown()","text":"

method to clean up temporary files triggered at shutdown.

Source code in tablite/base.py
def shutdown():\n    \"\"\"method to clean up temporary files triggered at shutdown.\"\"\"\n    for path in file_registry:\n        if Config.pid in str(path):  # safety feature to prevent rm -rf /\n            log.debug(f\"shutdown: running rmtree({path})\")\n            shutil.rmtree(path)\n
"},{"location":"reference/config/","title":"Config","text":""},{"location":"reference/config/#tablite.config","title":"tablite.config","text":""},{"location":"reference/config/#tablite.config-classes","title":"Classes","text":""},{"location":"reference/config/#tablite.config.Config","title":"tablite.config.Config","text":"

Bases: object

Config class for Tablite Tables.

The default location for the storage is loaded as

Config.workdir = pathlib.Path(os.environ.get(\"TABLITE_TMPDIR\", f\"{tempfile.gettempdir()}/tablite-tmp\"))

to overwrite, first import the config class, then set the new workdir.

>>> from tablite import config\n>>> from pathlib import Path\n>>> config.workdir = Path(\"/this/new/location\")\n

for every new table or record this path will be used.

PAGE_SIZE = 1_000_000 sets the page size limit.

Multiprocessing is enabled in one of three modes: AUTO = \"auto\" FALSE = \"sp\" FORCE = \"mp\"

MULTIPROCESSING_MODE = AUTO is default.

SINGLE_PROCESSING_LIMIT = 1_000_000 when the number of fields (rows x columns) exceed this value, multiprocessing is used.

"},{"location":"reference/config/#tablite.config.Config-attributes","title":"Attributes","text":""},{"location":"reference/config/#tablite.config.Config.USE_NIMPORTER","title":"tablite.config.Config.USE_NIMPORTER = os.environ.get('USE_NIMPORTER', 'true').lower() in ['1', 't', 'true', 'y', 'yes'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.ALLOW_CSV_READER_FALLTHROUGH","title":"tablite.config.Config.ALLOW_CSV_READER_FALLTHROUGH = os.environ.get('ALLOW_CSV_READER_FALLTHROUGH', 'true').lower() in ['1', 't', 'true', 'y', 'yes'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.NIM_SUPPORTED_CONV_TYPES","title":"tablite.config.Config.NIM_SUPPORTED_CONV_TYPES = ['Windows-1252', 'ISO-8859-1'] class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.workdir","title":"tablite.config.Config.workdir = pathlib.Path(os.environ.get('TABLITE_TMPDIR', f'{tempfile.gettempdir()}/tablite-tmp')) class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.pid","title":"tablite.config.Config.pid = f'pid-{os.getpid()}' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.PAGE_SIZE","title":"tablite.config.Config.PAGE_SIZE = 1000000 class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.ENCODING","title":"tablite.config.Config.ENCODING = 'UTF-8' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.DISK_LIMIT","title":"tablite.config.Config.DISK_LIMIT = int(10000000000.0) class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.SINGLE_PROCESSING_LIMIT","title":"tablite.config.Config.SINGLE_PROCESSING_LIMIT = 1000000 class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.AUTO","title":"tablite.config.Config.AUTO = 'auto' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.FALSE","title":"tablite.config.Config.FALSE = 'sp' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.FORCE","title":"tablite.config.Config.FORCE = 'mp' class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.MULTIPROCESSING_MODE","title":"tablite.config.Config.MULTIPROCESSING_MODE = AUTO class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config.TQDM_DISABLE","title":"tablite.config.Config.TQDM_DISABLE = False class-attribute instance-attribute","text":""},{"location":"reference/config/#tablite.config.Config-functions","title":"Functions","text":""},{"location":"reference/config/#tablite.config.Config.reset","title":"tablite.config.Config.reset() classmethod","text":"

Resets the config class to original values.

Source code in tablite/config.py
@classmethod\ndef reset(cls):\n    \"\"\"Resets the config class to original values.\"\"\"\n    for k, v in _default_values.items():\n        setattr(Config, k, v)\n
"},{"location":"reference/config/#tablite.config.Config.page_steps","title":"tablite.config.Config.page_steps(length) classmethod","text":"

an iterator that yield start and end in page sizes

YIELDS DESCRIPTION tuple

start:int, end:int

Source code in tablite/config.py
@classmethod\ndef page_steps(cls, length):\n    \"\"\"an iterator that yield start and end in page sizes\n\n    Yields:\n        tuple: start:int, end:int\n    \"\"\"\n    start, end = 0, 0\n    for _ in range(0, length + 1, cls.PAGE_SIZE):\n        start, end = end, min(end + cls.PAGE_SIZE, length)\n        yield start, end\n        if end == length:\n            return\n
"},{"location":"reference/core/","title":"Core","text":""},{"location":"reference/core/#tablite.core","title":"tablite.core","text":""},{"location":"reference/core/#tablite.core-attributes","title":"Attributes","text":""},{"location":"reference/core/#tablite.core.log","title":"tablite.core.log = logging.getLogger(__name__) module-attribute","text":""},{"location":"reference/core/#tablite.core-classes","title":"Classes","text":""},{"location":"reference/core/#tablite.core.Table","title":"tablite.core.Table(columns=None, headers=None, rows=None, _path=None)","text":"

Bases: Table

creates Table

PARAMETER DESCRIPTION EITHER

columns (dict, optional): dict with column names as keys, values as lists. Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})

Source code in tablite/core.py
def __init__(self, columns=None, headers=None, rows=None, _path=None) -> None:\n    \"\"\"creates Table\n\n    Args:\n        EITHER:\n            columns (dict, optional): dict with column names as keys, values as lists.\n            Example: t = Table(columns={\"a\": [1, 2], \"b\": [3, 4]})\n        OR\n            headers (list of strings, optional): list of column names.\n            rows (list of tuples or lists, optional): values for columns\n            Example: t = Table(headers=[\"a\", \"b\"], rows=[[1,3], [2,4]])\n    \"\"\"\n    super().__init__(columns, headers, rows, _path)\n
"},{"location":"reference/core/#tablite.core.Table-attributes","title":"Attributes","text":""},{"location":"reference/core/#tablite.core.Table.path","title":"tablite.core.Table.path = _path instance-attribute","text":""},{"location":"reference/core/#tablite.core.Table.columns","title":"tablite.core.Table.columns = {} instance-attribute","text":""},{"location":"reference/core/#tablite.core.Table.rows","title":"tablite.core.Table.rows property","text":"

enables row based iteration in python types.

Example:

for row in Table.rows:\n    print(row)\n

Yields: tuple: values is same order as columns.

"},{"location":"reference/core/#tablite.core.Table-functions","title":"Functions","text":""},{"location":"reference/core/#tablite.core.Table.__str__","title":"tablite.core.Table.__str__()","text":"Source code in tablite/base.py
def __str__(self):  # USER FUNCTION.\n    return f\"{self.__class__.__name__}({len(self.columns):,} columns, {len(self):,} rows)\"\n
"},{"location":"reference/core/#tablite.core.Table.__repr__","title":"tablite.core.Table.__repr__()","text":"Source code in tablite/base.py
def __repr__(self):\n    return self.__str__()\n
"},{"location":"reference/core/#tablite.core.Table.nbytes","title":"tablite.core.Table.nbytes()","text":"

finds the total bytes of the table on disk

RETURNS DESCRIPTION tuple

int: real bytes used on disk int: total bytes used if flattened

Source code in tablite/base.py
def nbytes(self):  # USER FUNCTION.\n    \"\"\"finds the total bytes of the table on disk\n\n    Returns:\n        tuple:\n            int: real bytes used on disk\n            int: total bytes used if flattened\n    \"\"\"\n    real = {}\n    total = 0\n    for column in self.columns.values():\n        for page in set(column.pages):\n            real[page] = page.path.stat().st_size\n        for page in column.pages:\n            total += real[page]\n    return sum(real.values()), total\n
"},{"location":"reference/core/#tablite.core.Table.items","title":"tablite.core.Table.items()","text":"

returns table as dict

RETURNS DESCRIPTION dict

Table as dict {column_name: [values], ...}

Source code in tablite/base.py
def items(self):  # USER FUNCTION.\n    \"\"\"returns table as dict\n\n    Returns:\n        dict: Table as dict `{column_name: [values], ...}`\n    \"\"\"\n    return {\n        name: column[:].tolist() for name, column in self.columns.items()\n    }.items()\n
"},{"location":"reference/core/#tablite.core.Table.__delitem__","title":"tablite.core.Table.__delitem__(key)","text":"

Examples:

>>> del table['a']  # removes column 'a'\n>>> del table[-3:]  # removes last 3 rows from all columns.\n
Source code in tablite/base.py
def __delitem__(self, key):  # USER FUNCTION.\n    \"\"\"\n    Examples:\n    ```\n    >>> del table['a']  # removes column 'a'\n    >>> del table[-3:]  # removes last 3 rows from all columns.\n    ```\n    \"\"\"\n    if isinstance(key, (int, slice)):\n        for column in self.columns.values():\n            del column[key]\n    elif key in self.columns:\n        del self.columns[key]\n    else:\n        raise KeyError(f\"Key not found: {key}\")\n
"},{"location":"reference/core/#tablite.core.Table.__setitem__","title":"tablite.core.Table.__setitem__(key, value)","text":"

table behaves like a dict. Args: key (str or hashable): column name value (iterable): list, tuple or nd.array with values.

As Table now accepts the keyword columns as a dict:

>>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n

and the header/data combinations:

>>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n

This has the side-benefit that tuples now can be used as headers.

Source code in tablite/base.py
def __setitem__(self, key, value):  # USER FUNCTION\n    \"\"\"table behaves like a dict.\n    Args:\n        key (str or hashable): column name\n        value (iterable): list, tuple or nd.array with values.\n\n    As Table now accepts the keyword `columns` as a dict:\n    ```\n    >>> t = Table(columns={'b':[4,5,6], 'c':[7,8,9]})\n    ```\n    and the header/data combinations:\n    ```\n    >>> t = Table(header=['b','c'], data=[[4,5,6],[7,8,9]])\n    ```\n    This has the side-benefit that tuples now can be used as headers.\n    \"\"\"\n    if value is None:\n        self.columns[key] = Column(self.path, value=None)\n    elif isinstance(value, (list, tuple)):\n        value = list_to_np_array(value)\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, (np.ndarray)):\n        self.columns[key] = Column(self.path, value)\n    elif isinstance(value, Column):\n        self.columns[key] = value\n    else:\n        raise TypeError(f\"{type(value)} not supported.\")\n
"},{"location":"reference/core/#tablite.core.Table.__getitem__","title":"tablite.core.Table.__getitem__(keys)","text":"

Enables selection of columns and rows

PARAMETER DESCRIPTION keys

TYPE: column name, integer or slice

Examples

>>>

10] selects first 10 rows from all columns

TYPE: table[

>>>

20:3] selects column 'b' and 'c' and 'a' twice for a slice.

TYPE: table['b', 'a', 'a', 'c', 2

Raises: KeyError: if key is not found. TypeError: if key is not a string, integer or slice.

RETURNS DESCRIPTION Table

returns columns in same order as selection.

Source code in tablite/base.py
def __getitem__(self, keys):  # USER FUNCTION\n    \"\"\"\n    Enables selection of columns and rows\n\n    Args:\n        keys (column name, integer or slice):\n        Examples:\n        ```\n        >>> table['a']                        selects column 'a'\n        >>> table[3]                          selects row 3 as a tuple.\n        >>> table[:10]                        selects first 10 rows from all columns\n        >>> table['a','b', slice(3,20,2)]     selects a slice from columns 'a' and 'b'\n        >>> table['b', 'a', 'a', 'c', 2:20:3] selects column 'b' and 'c' and 'a' twice for a slice.\n        >>> table[('b', 'a', 'a', 'c')]       selects columns 'b', 'a', 'a', and 'c' using a tuple.\n        ```\n    Raises:\n        KeyError: if key is not found.\n        TypeError: if key is not a string, integer or slice.\n\n    Returns:\n        Table: returns columns in same order as selection.\n    \"\"\"\n\n    if not isinstance(keys, tuple):\n        if isinstance(keys, list):\n            keys = tuple(keys)\n        else:\n            keys = (keys,)\n    if isinstance(keys[0], tuple):\n        keys = tuple(list(chain(*keys)))\n\n    integers = [i for i in keys if isinstance(i, int)]\n    if len(integers) == len(keys) == 1:  # return a single tuple.\n        keys = [slice(keys[0])]\n\n    column_names = [i for i in keys if isinstance(i, str)]\n    column_names = list(self.columns) if not column_names else column_names\n    not_found = [name for name in column_names if name not in self.columns]\n    if not_found:\n        raise KeyError(f\"keys not found: {', '.join(not_found)}\")\n\n    slices = [i for i in keys if isinstance(i, slice)]\n    slc = slice(0, len(self)) if not slices else slices[0]\n\n    if (\n        len(slices) == 0 and len(column_names) == 1\n    ):  # e.g. tbl['a'] or tbl['a'][:10]\n        col = self.columns[column_names[0]]\n        if slices:\n            return col[slc]  # return slice from column as list of values\n        else:\n            return col  # return whole column\n\n    elif len(integers) == 1:  # return a single tuple.\n        row_no = integers[0]\n        slc = slice(row_no, row_no + 1)\n        return tuple(self.columns[name][slc].tolist()[0] for name in column_names)\n\n    elif not slices:  # e.g. new table with N whole columns.\n        return self.__class__(\n            columns={name: self.columns[name] for name in column_names}\n        )\n\n    else:  # e.g. new table from selection of columns and slices.\n        t = self.__class__()\n        for name in column_names:\n            column = self.columns[name]\n\n            new_column = Column(t.path)  # create new Column.\n            for item in column.getpages(slc):\n                if isinstance(item, np.ndarray):\n                    new_column.extend(item)  # extend subslice (expensive)\n                elif isinstance(item, SimplePage):\n                    new_column.pages.append(item)  # extend page (cheap)\n                else:\n                    raise TypeError(f\"Bad item: {item}\")\n\n            # below:\n            # set the new column directly on t.columns.\n            # Do not use t[name] as that triggers __setitem__ again.\n            t.columns[name] = new_column\n\n        return t\n
"},{"location":"reference/core/#tablite.core.Table.__len__","title":"tablite.core.Table.__len__()","text":"Source code in tablite/base.py
def __len__(self):  # USER FUNCTION.\n    if not self.columns:\n        return 0\n    return max(len(c) for c in self.columns.values())\n
"},{"location":"reference/core/#tablite.core.Table.__eq__","title":"tablite.core.Table.__eq__(other) -> bool","text":"

Determines if two tables have identical content.

PARAMETER DESCRIPTION other

table for comparison

TYPE: Table

RETURNS DESCRIPTION bool

True if tables are identical.

TYPE: bool

Source code in tablite/base.py
def __eq__(self, other) -> bool:  # USER FUNCTION.\n    \"\"\"Determines if two tables have identical content.\n\n    Args:\n        other (Table): table for comparison\n\n    Returns:\n        bool: True if tables are identical.\n    \"\"\"\n    if isinstance(other, dict):\n        return self.items() == other.items()\n    if not isinstance(other, Table):\n        return False\n    if id(self) == id(other):\n        return True\n    if len(self) != len(other):\n        return False\n    if len(self) == len(other) == 0:\n        return True\n    if self.columns.keys() != other.columns.keys():\n        return False\n    for name, col in self.columns.items():\n        if not (col == other.columns[name]):\n            return False\n    return True\n
"},{"location":"reference/core/#tablite.core.Table.clear","title":"tablite.core.Table.clear()","text":"

clears the table. Like dict().clear()

Source code in tablite/base.py
def clear(self):  # USER FUNCTION.\n    \"\"\"clears the table. Like dict().clear()\"\"\"\n    self.columns.clear()\n
"},{"location":"reference/core/#tablite.core.Table.save","title":"tablite.core.Table.save(path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1)","text":"

saves table to compressed tpz file.

PARAMETER DESCRIPTION path

file destination.

TYPE: Path

compression_method

See zipfile compression methods. Defaults to ZIP_DEFLATED.

DEFAULT: ZIP_DEFLATED

compression_level

See zipfile compression levels. Defaults to 1.

DEFAULT: 1

The file format is as follows: .tpz is a gzip archive with table metadata captured as table.yml and the necessary set of pages saved as .npy files.

The zip contains table.yml which provides an overview of the data:

--------------------------------------\n%YAML 1.2                              yaml version\ncolumns:                               start of columns section.\n    name: \u201c\u5217 1\u201d                       name of column 1.\n        pages: [p1b1, p1b2]            list of pages in column 1.\n    name: \u201c\u5217 2\u201d                       name of column 2\n        pages: [p2b1, p2b2]            list of pages in column 2.\n----------------------------------------\n
Source code in tablite/base.py
def save(\n    self, path, compression_method=zipfile.ZIP_DEFLATED, compression_level=1\n):  # USER FUNCTION.\n    \"\"\"saves table to compressed tpz file.\n\n    Args:\n        path (Path): file destination.\n        compression_method: See zipfile compression methods. Defaults to ZIP_DEFLATED.\n        compression_level: See zipfile compression levels. Defaults to 1.\n        The default settings produce 80% compression at 10% slowdown.\n\n    The file format is as follows:\n    .tpz is a gzip archive with table metadata captured as table.yml\n    and the necessary set of pages saved as .npy files.\n\n    The zip contains table.yml which provides an overview of the data:\n    ```\n    --------------------------------------\n    %YAML 1.2                              yaml version\n    columns:                               start of columns section.\n        name: \u201c\u5217 1\u201d                       name of column 1.\n            pages: [p1b1, p1b2]            list of pages in column 1.\n        name: \u201c\u5217 2\u201d                       name of column 2\n            pages: [p2b1, p2b2]            list of pages in column 2.\n    ----------------------------------------\n    ```\n    \"\"\"\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path += \".tpz\"\n\n    # create yaml document\n    _page_counter = 0\n    d = {}\n    cols = {}\n    for name, col in self.columns.items():\n        type_check(col, Column)\n        cols[name] = {\"pages\": [p.path.name for p in col.pages]}\n        _page_counter += len(col.pages)\n    d[\"columns\"] = cols\n    yml = yaml.safe_dump(\n        d, sort_keys=False, allow_unicode=True, default_flow_style=None\n    )\n\n    _file_counter = 0\n    with zipfile.ZipFile(\n        path, \"w\", compression=compression_method, compresslevel=compression_level\n    ) as f:\n        log.debug(f\"writing .tpz to {path} with\\n{yml}\")\n        f.writestr(\"table.yml\", yml)\n        for name, col in self.columns.items():\n            for page in set(\n                col.pages\n            ):  # set of pages! remember t *= 1000 repeats t 1000x\n                with open(page.path, \"rb\", buffering=0) as raw_io:\n                    f.writestr(page.path.name, raw_io.read())\n                _file_counter += 1\n                log.debug(f\"adding Page {page.path}\")\n\n        _fields = len(self) * len(self.columns)\n        _avg = _fields // _page_counter\n        log.debug(\n            f\"Wrote {_fields:,} on {_page_counter:,} pages in {_file_counter} files: {_avg} fields/page\"\n        )\n
"},{"location":"reference/core/#tablite.core.Table.load","title":"tablite.core.Table.load(path, tqdm=_tqdm) classmethod","text":"

loads a table from .tpz file. See also Table.save for details on the file format.

PARAMETER DESCRIPTION path

source file

TYPE: Path

RETURNS DESCRIPTION Table

table in read-only mode.

Source code in tablite/base.py
@classmethod\ndef load(cls, path, tqdm=_tqdm):  # USER FUNCTION.\n    \"\"\"loads a table from .tpz file.\n    See also Table.save for details on the file format.\n\n    Args:\n        path (Path): source file\n\n    Returns:\n        Table: table in read-only mode.\n    \"\"\"\n    type_check(path, Path)\n    log.debug(f\"loading {path}\")\n    with zipfile.ZipFile(path, \"r\") as f:\n        yml = f.read(\"table.yml\")\n        metadata = yaml.safe_load(yml)\n        t = cls()\n\n        page_count = sum([len(c[\"pages\"]) for c in metadata[\"columns\"].values()])\n\n        with tqdm(\n            total=page_count,\n            desc=f\"loading '{path.name}' file\",\n            disable=Config.TQDM_DISABLE,\n        ) as pbar:\n            for name, d in metadata[\"columns\"].items():\n                column = Column(t.path)\n                for page in d[\"pages\"]:\n                    bytestream = io.BytesIO(f.read(page))\n                    data = np.load(bytestream, allow_pickle=True, fix_imports=False)\n                    column.extend(data)\n                    pbar.update(1)\n                t.columns[name] = column\n    update_access_time(path)\n    return t\n
"},{"location":"reference/core/#tablite.core.Table.copy","title":"tablite.core.Table.copy()","text":"Source code in tablite/base.py
def copy(self):\n    cls = type(self)\n    t = cls()\n    for name, column in self.columns.items():\n        new = Column(t.path)\n        new.pages = column.pages[:]\n        t.columns[name] = new\n    return t\n
"},{"location":"reference/core/#tablite.core.Table.__imul__","title":"tablite.core.Table.__imul__(other)","text":"

Repeats instance of table N times.

Like list: t = t * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

Source code in tablite/base.py
def __imul__(self, other):\n    \"\"\"Repeats instance of table N times.\n\n    Like list: `t = t * N`\n\n    Args:\n        other (int): multiplier\n    \"\"\"\n    if not (isinstance(other, int) and other > 0):\n        raise TypeError(\n            f\"a table can be repeated an integer number of times, not {type(other)} number of times\"\n        )\n    for col in self.columns.values():\n        col *= other\n    return self\n
"},{"location":"reference/core/#tablite.core.Table.__mul__","title":"tablite.core.Table.__mul__(other)","text":"

Repeat table N times. Like list: new = old * N

PARAMETER DESCRIPTION other

multiplier

TYPE: int

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __mul__(self, other):\n    \"\"\"Repeat table N times.\n    Like list: `new = old * N`\n\n    Args:\n        other (int): multiplier\n\n    Returns:\n        Table\n    \"\"\"\n    new = self.copy()\n    return new.__imul__(other)\n
"},{"location":"reference/core/#tablite.core.Table.__iadd__","title":"tablite.core.Table.__iadd__(other)","text":"

Concatenates tables with same column names.

Like list: table_1 += table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION None

self is updated.

Source code in tablite/base.py
def __iadd__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_1 += table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        None: self is updated.\n    \"\"\"\n    type_check(other, Table)\n    for name in self.columns.keys():\n        if name not in other.columns:\n            raise ValueError(f\"{name} not in other\")\n    for name in other.columns.keys():\n        if name not in self.columns:\n            raise ValueError(f\"{name} missing from self\")\n\n    for name, column in self.columns.items():\n        other_col = other.columns.get(name, None)\n        column.pages.extend(other_col.pages[:])\n    return self\n
"},{"location":"reference/core/#tablite.core.Table.__add__","title":"tablite.core.Table.__add__(other)","text":"

Concatenates tables with same column names.

Like list: table_3 = table_1 + table_2

RAISES DESCRIPTION ValueError

If column names don't match.

RETURNS DESCRIPTION

Table

Source code in tablite/base.py
def __add__(self, other):\n    \"\"\"Concatenates tables with same column names.\n\n    Like list: `table_3 = table_1 + table_2`\n\n    Args:\n        other (Table)\n\n    Raises:\n        ValueError: If column names don't match.\n\n    Returns:\n        Table\n    \"\"\"\n    type_check(other, Table)\n    cp = self.copy()\n    cp += other\n    return cp\n
"},{"location":"reference/core/#tablite.core.Table.add_rows","title":"tablite.core.Table.add_rows(*args, **kwargs)","text":"

its more efficient to add many rows at once.

if both args and kwargs, then args are added first, followed by kwargs.

supported cases:

>>> t = Table()\n>>> t.add_columns('row','A','B','C')\n>>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n>>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n>>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n>>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n>>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n>>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n>>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n>>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n>>> t.add_rows(\n    {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n    )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n>>> t.add_rows( *[\n    {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n    {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n    ])                                                  # (10) list of dicts as args\n>>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n
Source code in tablite/base.py
def add_rows(self, *args, **kwargs):\n    \"\"\"its more efficient to add many rows at once.\n\n    if both args and kwargs, then args are added first, followed by kwargs.\n\n    supported cases:\n    ```\n    >>> t = Table()\n    >>> t.add_columns('row','A','B','C')\n    >>> t.add_rows(1, 1, 2, 3)                              # (1) individual values as args\n    >>> t.add_rows([2, 1, 2, 3])                            # (2) list of values as args\n    >>> t.add_rows((3, 1, 2, 3))                            # (3) tuple of values as args\n    >>> t.add_rows(*(4, 1, 2, 3))                           # (4) unpacked tuple becomes arg like (1)\n    >>> t.add_rows(row=5, A=1, B=2, C=3)                    # (5) kwargs\n    >>> t.add_rows(**{'row': 6, 'A': 1, 'B': 2, 'C': 3})    # (6) dict / json interpreted a kwargs\n    >>> t.add_rows((7, 1, 2, 3), (8, 4, 5, 6))              # (7) two (or more) tuples as args\n    >>> t.add_rows([9, 1, 2, 3], [10, 4, 5, 6])             # (8) two or more lists as rgs\n    >>> t.add_rows(\n        {'row': 11, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 12, 'A': 4, 'B': 5, 'C': 6}\n        )                                                   # (9) two (or more) dicts as args - roughly comma sep'd json.\n    >>> t.add_rows( *[\n        {'row': 13, 'A': 1, 'B': 2, 'C': 3},\n        {'row': 14, 'A': 1, 'B': 2, 'C': 3}\n        ])                                                  # (10) list of dicts as args\n    >>> t.add_rows(row=[15,16], A=[1,1], B=[2,2], C=[3,3])  # (11) kwargs with lists as values\n    ```\n\n    \"\"\"\n    if not Table._add_row_slow_warning:\n        warnings.warn(\n            \"add_rows is slow. Consider using add_columns and then assigning values to the columns directly.\"\n        )\n        Table._add_row_slow_warning = True\n\n    if args:\n        if not all(isinstance(i, (list, tuple, dict)) for i in args):  # 1,4\n            args = [args]\n\n        if all(isinstance(i, (list, tuple, dict)) for i in args):  # 2,3,7,8\n            # 1. turn the data into columns:\n\n            d = {n: [] for n in self.columns}\n            for arg in args:\n                if len(arg) != len(self.columns):\n                    raise ValueError(\n                        f\"len({arg})== {len(arg)}, but there are {len(self.columns)} columns\"\n                    )\n\n                if isinstance(arg, dict):\n                    for k, v in arg.items():  # 7,8\n                        d[k].append(v)\n\n                elif isinstance(arg, (list, tuple)):  # 2,3\n                    for n, v in zip(self.columns, arg):\n                        d[n].append(v)\n\n                else:\n                    raise TypeError(f\"{arg}?\")\n            # 2. extend the columns\n            for n, values in d.items():\n                col = self.columns[n]\n                col.extend(list_to_np_array(values))\n\n    if kwargs:\n        if isinstance(kwargs, dict):\n            if all(isinstance(v, (list, tuple)) for v in kwargs.values()):\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(list_to_np_array(v))\n            else:\n                for k, v in kwargs.items():\n                    col = self.columns[k]\n                    col.extend(np.array([v]))\n        else:\n            raise ValueError(f\"format not recognised: {kwargs}\")\n\n    return\n
"},{"location":"reference/core/#tablite.core.Table.add_columns","title":"tablite.core.Table.add_columns(*names)","text":"

Adds column names to table.

Source code in tablite/base.py
def add_columns(self, *names):\n    \"\"\"Adds column names to table.\"\"\"\n    for name in names:\n        self.columns[name] = Column(self.path)\n
"},{"location":"reference/core/#tablite.core.Table.add_column","title":"tablite.core.Table.add_column(name, data=None)","text":"

verbose alias for table[name] = data, that checks if name already exists

PARAMETER DESCRIPTION name

column name

TYPE: str

data

values. Defaults to None.

TYPE: list,tuple) DEFAULT: None

RAISES DESCRIPTION TypeError

name isn't string

ValueError

name already exists

Source code in tablite/base.py
def add_column(self, name, data=None):\n    \"\"\"verbose alias for table[name] = data, that checks if name already exists\n\n    Args:\n        name (str): column name\n        data ((list,tuple), optional): values. Defaults to None.\n\n    Raises:\n        TypeError: name isn't string\n        ValueError: name already exists\n    \"\"\"\n    if not isinstance(name, str):\n        raise TypeError(\"expected name as string\")\n    if name in self.columns:\n        raise ValueError(f\"{name} already in {self.columns}\")\n    self.__setitem__(name, data)\n
"},{"location":"reference/core/#tablite.core.Table.stack","title":"tablite.core.Table.stack(other)","text":"

returns the joint stack of tables with overlapping column names. Example:

| Table A|  +  | Table B| = |  Table AB |\n| A| B| C|     | A| B| D|   | A| B| C| -|\n                            | A| B| -| D|\n
Source code in tablite/base.py
def stack(self, other):\n    \"\"\"\n    returns the joint stack of tables with overlapping column names.\n    Example:\n    ```\n    | Table A|  +  | Table B| = |  Table AB |\n    | A| B| C|     | A| B| D|   | A| B| C| -|\n                                | A| B| -| D|\n    ```\n    \"\"\"\n    if not isinstance(other, Table):\n        raise TypeError(f\"stack only works for Table, not {type(other)}\")\n\n    cp = self.copy()\n    for name, col2 in other.columns.items():\n        if name not in cp.columns:\n            cp[name] = [None] * len(self)\n        cp[name].pages.extend(col2.pages[:])\n\n    for name in self.columns:\n        if name not in other.columns:\n            if len(cp) > 0:\n                cp[name].extend(np.array([None] * len(other)))\n    return cp\n
"},{"location":"reference/core/#tablite.core.Table.types","title":"tablite.core.Table.types()","text":"

returns nested dict of data types in the form: {column name: {python type class: number of instances }, ... }

example:

>>> t.types()\n{\n    'A': {<class 'str'>: 7},\n    'B': {<class 'int'>: 7}\n}\n
Source code in tablite/base.py
def types(self):\n    \"\"\"\n    returns nested dict of data types in the form:\n    `{column name: {python type class: number of instances }, ... }`\n\n    example:\n    ```\n    >>> t.types()\n    {\n        'A': {<class 'str'>: 7},\n        'B': {<class 'int'>: 7}\n    }\n    ```\n    \"\"\"\n    d = {}\n    for name, col in self.columns.items():\n        assert isinstance(col, Column)\n        d[name] = col.types()\n    return d\n
"},{"location":"reference/core/#tablite.core.Table.display_dict","title":"tablite.core.Table.display_dict(slice_=None, blanks=None, dtype=False)","text":"

helper for creating dict for display.

PARAMETER DESCRIPTION slice_

python slice. Defaults to None.

TYPE: slice DEFAULT: None

blanks

fill value for None. Defaults to None.

TYPE: optional DEFAULT: None

dtype

Adds datatype to each column. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION TypeError

slice_ must be None or slice.

RETURNS DESCRIPTION dict

from Table.

Source code in tablite/base.py
def display_dict(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"helper for creating dict for display.\n\n    Args:\n        slice_ (slice, optional): python slice. Defaults to None.\n        blanks (optional): fill value for `None`. Defaults to None.\n        dtype (bool, optional): Adds datatype to each column. Defaults to False.\n\n    Raises:\n        TypeError: slice_ must be None or slice.\n\n    Returns:\n        dict: from Table.\n    \"\"\"\n    if not self.columns:\n        print(\"Empty Table\")\n        return\n\n    def datatype(col):  # PRIVATE\n        \"\"\"creates label for column datatype.\"\"\"\n        types = col.types()\n        if len(types) == 1:\n            dt, _ = types.popitem()\n            typ = dt.__name__\n        else:\n            typ = \"mixed\"\n        return typ\n\n    row_count_tags = [\"#\", \"~\", \"*\"]\n    cols = set(self.columns)\n    for n, tag in product(range(1, 6), row_count_tags):\n        if n * tag not in cols:\n            tag = n * tag\n            break\n\n    if not isinstance(slice_, (slice, type(None))):\n        raise TypeError(f\"slice_ must be None or slice, not {type(slice_)}\")\n    if isinstance(slice_, slice):\n        slc = slice_\n    if slice_ is None:\n        if len(self) <= 20:\n            slc = slice(0, 20, 1)\n        else:\n            slc = None\n\n    n = len(self)\n    if slc:  # either we want slc or we want everything.\n        row_no = list(range(*slc.indices(len(self))))\n        data = {tag: [f\"{i:,}\".rjust(2) for i in row_no]}\n        for name, col in self.columns.items():\n            data[name] = list(chain(iter(col), repeat(blanks, times=n - len(col))))[\n                slc\n            ]\n    else:\n        data = {}\n        j = int(math.ceil(math.log10(n)) / 3) + len(str(n))\n        row_no = (\n            [f\"{i:,}\".rjust(j) for i in range(7)]\n            + [\"...\"]\n            + [f\"{i:,}\".rjust(j) for i in range(n - 7, n)]\n        )\n        data = {tag: row_no}\n\n        for name, col in self.columns.items():\n            if len(col) == n:\n                row = col[:7].tolist() + [\"...\"] + col[-7:].tolist()\n            else:\n                empty = [blanks] * 7\n                head = (col[:7].tolist() + empty)[:7]\n                tail = (col[n - 7 :].tolist() + empty)[-7:]\n                row = head + [\"...\"] + tail\n            data[name] = row\n\n    if dtype:\n        for name, values in data.items():\n            if name in self.columns:\n                col = self.columns[name]\n                values.insert(0, datatype(col))\n            else:\n                values.insert(0, \"row\")\n\n    return data\n
"},{"location":"reference/core/#tablite.core.Table.to_ascii","title":"tablite.core.Table.to_ascii(slice_=None, blanks=None, dtype=False)","text":"

returns ascii view of table as string.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def to_ascii(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"returns ascii view of table as string.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n\n    def adjust(v, length):  # PRIVATE FUNCTION\n        \"\"\"whitespace justifies field values based on datatype\"\"\"\n        if v is None:\n            return str(blanks).ljust(length)\n        elif isinstance(v, str):\n            return v.ljust(length)\n        else:\n            return str(v).rjust(length)\n\n    if not self.columns:\n        return str(self)\n\n    d = {}\n    for name, values in self.display_dict(\n        slice_=slice_, blanks=blanks, dtype=dtype\n    ).items():\n        as_text = [str(v) for v in values] + [str(name)]\n        width = max(len(i) for i in as_text)\n        new_name = name.center(width, \" \")\n        if dtype:\n            values[0] = values[0].center(width, \" \")\n        d[new_name] = [adjust(v, width) for v in values]\n\n    rows = dict_to_rows(d)\n    s = []\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n    s.append(\"|\" + \"|\".join(rows[0]) + \"|\")  # column names\n    start = 1\n    if dtype:\n        s.append(\"|\" + \"|\".join(rows[1]) + \"|\")  # datatypes\n        start = 2\n\n    s.append(\"+\" + \"+\".join([\"-\" * len(n) for n in rows[0]]) + \"+\")\n    for row in rows[start:]:\n        s.append(\"|\" + \"|\".join(row) + \"|\")\n    s.append(\"+\" + \"+\".join([\"=\" * len(n) for n in rows[0]]) + \"+\")\n\n    if len(set(len(c) for c in self.columns.values())) != 1:\n        warning = f\"Warning: Columns have different lengths. {blanks} is used as fill value.\"\n        s.append(warning)\n\n    return \"\\n\".join(s)\n
"},{"location":"reference/core/#tablite.core.Table.show","title":"tablite.core.Table.show(slice_=None, blanks=None, dtype=False)","text":"

prints ascii view of table.

PARAMETER DESCRIPTION slice_

slice to determine table snippet.

TYPE: slice DEFAULT: None

blanks

value for whitespace. Defaults to None.

TYPE: str DEFAULT: None

dtype

adds subheader with datatype for column. Defaults to False.

TYPE: bool DEFAULT: False

Source code in tablite/base.py
def show(self, slice_=None, blanks=None, dtype=False):\n    \"\"\"prints ascii view of table.\n\n    Args:\n        slice_ (slice, optional): slice to determine table snippet.\n        blanks (str, optional): value for whitespace. Defaults to None.\n        dtype (bool, optional): adds subheader with datatype for column. Defaults to False.\n    \"\"\"\n    print(self.to_ascii(slice_=slice_, blanks=blanks, dtype=dtype))\n
"},{"location":"reference/core/#tablite.core.Table.to_dict","title":"tablite.core.Table.to_dict(columns=None, slice_=None)","text":"

columns: list of column names. Default is None == all columns. slice_: slice. Default is None == all rows.

returns: dict with columns as keys and lists of values.

Example:

>>> t.show()\n+===+===+===+\n| # | a | b |\n|row|int|int|\n+---+---+---+\n| 0 |  1|  3|\n| 1 |  2|  4|\n+===+===+===+\n>>> t.to_dict()\n{'a':[1,2], 'b':[3,4]}\n
Source code in tablite/base.py
def to_dict(self, columns=None, slice_=None):\n    \"\"\"\n    columns: list of column names. Default is None == all columns.\n    slice_: slice. Default is None == all rows.\n\n    returns: dict with columns as keys and lists of values.\n\n    Example:\n    ```\n    >>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  3|\n    | 1 |  2|  4|\n    +===+===+===+\n    >>> t.to_dict()\n    {'a':[1,2], 'b':[3,4]}\n    ```\n\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n    assert isinstance(slice_, slice)\n\n    if columns is None:\n        columns = list(self.columns.keys())\n    if not isinstance(columns, list):\n        raise TypeError(\"expected columns as list of strings\")\n\n    return {name: list(self.columns[name][slice_]) for name in columns}\n
"},{"location":"reference/core/#tablite.core.Table.as_json_serializable","title":"tablite.core.Table.as_json_serializable(row_count='row id', start_on=1, columns=None, slice_=None)","text":"

provides a JSON compatible format of the table.

PARAMETER DESCRIPTION row_count

Label for row counts. Defaults to \"row id\".

TYPE: str DEFAULT: 'row id'

start_on

row counts starts by default on 1.

TYPE: int DEFAULT: 1

columns

Column names. Defaults to None which returns all columns.

TYPE: list of str DEFAULT: None

slice_

selector. Defaults to None which returns [:]

TYPE: slice DEFAULT: None

RETURNS DESCRIPTION

JSON serializable dict: All python datatypes have been converted to JSON compliant data.

Source code in tablite/base.py
def as_json_serializable(\n    self, row_count=\"row id\", start_on=1, columns=None, slice_=None\n):\n    \"\"\"provides a JSON compatible format of the table.\n\n    Args:\n        row_count (str, optional): Label for row counts. Defaults to \"row id\".\n        start_on (int, optional): row counts starts by default on 1.\n        columns (list of str, optional): Column names.\n            Defaults to None which returns all columns.\n        slice_ (slice, optional): selector. Defaults to None which returns [:]\n\n    Returns:\n        JSON serializable dict: All python datatypes have been converted to JSON compliant data.\n    \"\"\"\n    if slice_ is None:\n        slice_ = slice(0, len(self))\n\n    assert isinstance(slice_, slice)\n    new = {\"columns\": {}, \"total_rows\": len(self)}\n    if row_count is not None:\n        new[\"columns\"][row_count] = [\n            i + start_on for i in range(*slice_.indices(len(self)))\n        ]\n\n    d = self.to_dict(columns, slice_=slice_)\n    for k, data in d.items():\n        new_k = unique_name(\n            k, new[\"columns\"]\n        )  # used to avoid overwriting the `row id` key.\n        new[\"columns\"][new_k] = [\n            DataTypes.to_json(v) for v in data\n        ]  # deal with non-json datatypes.\n    return new\n
"},{"location":"reference/core/#tablite.core.Table.index","title":"tablite.core.Table.index(*args)","text":"

param: *args: column names returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}

Examples:

>>> table6 = Table()\n>>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n>>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n
>>> table6.index('A')  # single key.\n{('Alice',): [0],\n ('Bob',): [1, 2],\n ('Ben',): [3, 5],\n ('Charlie',): [4],\n ('Albert',): [6]})\n
>>> table6.index('A', 'B')  # multiple keys.\n{('Alice', 'Alison'): [0],\n ('Bob', 'Marley'): [1],\n ('Bob', 'Dylan'): [2],\n ('Ben', 'Affleck'): [3],\n ('Charlie', 'Hepburn'): [4],\n ('Ben', 'Barnes'): [5],\n ('Albert', 'Einstein'): [6]})\n
Source code in tablite/base.py
def index(self, *args):\n    \"\"\"\n    param: *args: column names\n    returns multikey index on the columns as d[(key tuple, )] = {index1, index2, ...}\n\n    Examples:\n        ```\n        >>> table6 = Table()\n        >>> table6['A'] = ['Alice', 'Bob', 'Bob', 'Ben', 'Charlie', 'Ben','Albert']\n        >>> table6['B'] = ['Alison', 'Marley', 'Dylan', 'Affleck', 'Hepburn', 'Barnes', 'Einstein']\n        ```\n\n        ```\n        >>> table6.index('A')  # single key.\n        {('Alice',): [0],\n         ('Bob',): [1, 2],\n         ('Ben',): [3, 5],\n         ('Charlie',): [4],\n         ('Albert',): [6]})\n        ```\n\n        ```\n        >>> table6.index('A', 'B')  # multiple keys.\n        {('Alice', 'Alison'): [0],\n         ('Bob', 'Marley'): [1],\n         ('Bob', 'Dylan'): [2],\n         ('Ben', 'Affleck'): [3],\n         ('Charlie', 'Hepburn'): [4],\n         ('Ben', 'Barnes'): [5],\n         ('Albert', 'Einstein'): [6]})\n        ```\n\n    \"\"\"\n    idx = defaultdict(list)\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in enumerate(zip(*iterators)):\n        key = tuple(numpy_to_python(k) for k in key)\n        idx[key].append(ix)\n    return idx\n
"},{"location":"reference/core/#tablite.core.Table.unique_index","title":"tablite.core.Table.unique_index(*args, tqdm=_tqdm)","text":"

generates the index of unique rows given a list of column names

PARAMETER DESCRIPTION *args

columns names

TYPE: any DEFAULT: ()

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION

np.array(int64): indices of unique records.

Source code in tablite/base.py
def unique_index(self, *args, tqdm=_tqdm):\n    \"\"\"generates the index of unique rows given a list of column names\n\n    Args:\n        *args (any): columns names\n        tqdm (tqdm, optional): Defaults to _tqdm.\n\n    Returns:\n        np.array(int64): indices of unique records.\n    \"\"\"\n    if not args:\n        raise ValueError(\"*args (column names) is required\")\n    seen = set()\n    unique = set()\n    iterators = [iter(self.columns[c]) for c in args]\n    for ix, key in tqdm(enumerate(zip(*iterators)), disable=Config.TQDM_DISABLE):\n        key_hash = hash(tuple(numpy_to_python(k) for k in key))\n        if key_hash in seen:\n            continue\n        else:\n            seen.add(key_hash)\n            unique.add(ix)\n    return np.array(sorted(unique))\n
"},{"location":"reference/core/#tablite.core.Table.from_file","title":"tablite.core.Table.from_file(path, columns=None, first_row_has_headers=True, header_row_index=0, encoding=None, start=0, limit=sys.maxsize, sheet=None, guess_datatypes=True, newline='\\n', text_qualifier=None, delimiter=None, strip_leading_and_tailing_whitespace=True, text_escape_openings='', text_escape_closures='', tqdm=_tqdm) -> Table classmethod","text":"
    reads path and imports 1 or more tables\n\n    REQUIRED\n    --------\n    path: pathlib.Path or str\n        selection of filereader uses path.suffix.\n        See `filereaders`.\n\n    OPTIONAL\n    --------\n    columns:\n        None: (default) All columns will be imported.\n        List: only column names from list will be imported (if present in file)\n              e.g. ['A', 'B', 'C', 'D']\n\n              datatype is detected using Datatypes.guess(...)\n              You can try it out with:\n              >> from tablite.datatypes import DataTypes\n              >> DataTypes.guess(['001','100'])\n              [1,100]\n\n              if the format cannot be achieved the read type is kept.\n        Excess column names are ignored.\n\n        HINT: To get the head of file use:\n        >>> from tablite.tools import head\n        >>> head = head(path)\n\n    first_row_has_headers: boolean\n        True: (default) first row is used as column names.\n        False: integers are used as column names.\n\n    encoding: str. Defaults to None (autodetect using n bytes).\n        n is declared in filereader_utils as ENCODING_GUESS_BYTES\n\n    start: the first line to be read (default: 0)\n\n    limit: the number of lines to be read from start (default sys.maxint ~ 2**63)\n\n    OPTIONAL FOR EXCEL AND ODS READERS\n    ----------------------------------\n\n    sheet: sheet name to import  (applicable to excel- and ods-reader only)\n        e.g. 'sheet_1'\n        sheets not found excess names are ignored.\n\n    OPTIONAL FOR TEXT READERS\n    -------------------------\n    guess_datatype: bool\n        True: (default) datatypes are guessed using DataTypes.guess(...)\n        False: all data is imported as strings.\n\n    newline: newline character (applicable to text_reader only)\n        str: '\n

' (default) or ' '

    text_qualifier: character (applicable to text_reader only)\n        None: No text qualifier is used.\n        str: \" or '\n\n    delimiter: character (applicable to text_reader only)\n        None: file suffix is used to determine field delimiter:\n            .txt: \"|\"\n            .csv: \",\",\n            .ssv: \";\"\n            .tsv: \" \" (tab)\n\n    strip_leading_and_tailing_whitespace: bool:\n        True: default\n\n    text_escape_openings: (applicable to text_reader only)\n        None: default\n        str: list of characters such as ([{\n\n    text_escape_closures: (applicable to text_reader only)\n        None: default\n        str: list of characters such as }])\n
Source code in tablite/core.py
@classmethod\ndef from_file(\n    cls,\n    path,\n    columns=None,\n    first_row_has_headers=True,\n    header_row_index=0,\n    encoding=None,\n    start=0,\n    limit=sys.maxsize,\n    sheet=None,\n    guess_datatypes=True,\n    newline=\"\\n\",\n    text_qualifier=None,\n    delimiter=None,\n    strip_leading_and_tailing_whitespace=True,\n    text_escape_openings=\"\",\n    text_escape_closures=\"\",\n    tqdm=_tqdm,\n) -> \"Table\":\n    \"\"\"\n    reads path and imports 1 or more tables\n\n    REQUIRED\n    --------\n    path: pathlib.Path or str\n        selection of filereader uses path.suffix.\n        See `filereaders`.\n\n    OPTIONAL\n    --------\n    columns:\n        None: (default) All columns will be imported.\n        List: only column names from list will be imported (if present in file)\n              e.g. ['A', 'B', 'C', 'D']\n\n              datatype is detected using Datatypes.guess(...)\n              You can try it out with:\n              >> from tablite.datatypes import DataTypes\n              >> DataTypes.guess(['001','100'])\n              [1,100]\n\n              if the format cannot be achieved the read type is kept.\n        Excess column names are ignored.\n\n        HINT: To get the head of file use:\n        >>> from tablite.tools import head\n        >>> head = head(path)\n\n    first_row_has_headers: boolean\n        True: (default) first row is used as column names.\n        False: integers are used as column names.\n\n    encoding: str. Defaults to None (autodetect using n bytes).\n        n is declared in filereader_utils as ENCODING_GUESS_BYTES\n\n    start: the first line to be read (default: 0)\n\n    limit: the number of lines to be read from start (default sys.maxint ~ 2**63)\n\n    OPTIONAL FOR EXCEL AND ODS READERS\n    ----------------------------------\n\n    sheet: sheet name to import  (applicable to excel- and ods-reader only)\n        e.g. 'sheet_1'\n        sheets not found excess names are ignored.\n\n    OPTIONAL FOR TEXT READERS\n    -------------------------\n    guess_datatype: bool\n        True: (default) datatypes are guessed using DataTypes.guess(...)\n        False: all data is imported as strings.\n\n    newline: newline character (applicable to text_reader only)\n        str: '\\n' (default) or '\\r\\n'\n\n    text_qualifier: character (applicable to text_reader only)\n        None: No text qualifier is used.\n        str: \" or '\n\n    delimiter: character (applicable to text_reader only)\n        None: file suffix is used to determine field delimiter:\n            .txt: \"|\"\n            .csv: \",\",\n            .ssv: \";\"\n            .tsv: \"\\t\" (tab)\n\n    strip_leading_and_tailing_whitespace: bool:\n        True: default\n\n    text_escape_openings: (applicable to text_reader only)\n        None: default\n        str: list of characters such as ([{\n\n    text_escape_closures: (applicable to text_reader only)\n        None: default\n        str: list of characters such as }])\n\n    \"\"\"\n    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n\n    if not path.exists():\n        raise FileNotFoundError(f\"file not found: {path}\")\n\n    if not isinstance(start, int) or not 0 <= start <= sys.maxsize:\n        raise ValueError(f\"start {start} not in range(0,{sys.maxsize})\")\n\n    if not isinstance(limit, int) or not 0 < limit <= sys.maxsize:\n        raise ValueError(f\"limit {limit} not in range(0,{sys.maxsize})\")\n\n    if not isinstance(first_row_has_headers, bool):\n        raise TypeError(\"first_row_has_headers is not bool\")\n\n    import_as = path.suffix\n    if import_as.startswith(\".\"):\n        import_as = import_as[1:]\n\n    reader = import_utils.file_readers.get(import_as, None)\n    if reader is None:\n        raise ValueError(f\"{import_as} is not in supported format: {import_utils.valid_readers}\")\n\n    additional_configs = {\"tqdm\": tqdm}\n    if reader == import_utils.text_reader:\n        # here we inject tqdm, if tqdm is not provided, use generic iterator\n        # fmt:off\n        config = (path, columns, first_row_has_headers, header_row_index, encoding, start, limit, newline,\n                  guess_datatypes, text_qualifier, strip_leading_and_tailing_whitespace,\n                  delimiter, text_escape_openings, text_escape_closures)\n        # fmt:on\n\n    elif reader == import_utils.from_html:\n        config = (path,)\n    elif reader == import_utils.from_hdf5:\n        config = (path,)\n\n    elif reader == import_utils.excel_reader:\n        # config = path, first_row_has_headers, sheet, columns, start, limit\n        config = (\n            path,\n            first_row_has_headers,\n            header_row_index,\n            sheet,\n            columns,\n            start,\n            limit,\n        )  # if file length changes - re-import.\n\n    if reader == import_utils.ods_reader:\n        # path, first_row_has_headers=True, sheet=None, columns=None, start=0, limit=sys.maxsize,\n        config = (\n            str(path),\n            first_row_has_headers,\n            header_row_index,\n            sheet,\n            columns,\n            start,\n            limit,\n        )  # if file length changes - re-import.\n\n    # At this point the import config seems valid.\n    # Now we check if the file already has been imported.\n\n    # publish the settings\n    return reader(cls, *config, **additional_configs)\n
"},{"location":"reference/core/#tablite.core.Table.from_pandas","title":"tablite.core.Table.from_pandas(df) classmethod","text":"

Creates Table using pd.to_dict('list')

similar to:

>>> import pandas as pd\n>>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n>>> df\n    a  b\n    0  1  4\n    1  2  5\n    2  3  6\n>>> df.to_dict('list')\n{'a': [1, 2, 3], 'b': [4, 5, 6]}\n>>> t = Table.from_dict(df.to_dict('list))\n>>> t.show()\n    +===+===+===+\n    | # | a | b |\n    |row|int|int|\n    +---+---+---+\n    | 0 |  1|  4|\n    | 1 |  2|  5|\n    | 2 |  3|  6|\n    +===+===+===+\n
Source code in tablite/core.py
@classmethod\ndef from_pandas(cls, df):\n    \"\"\"\n    Creates Table using pd.to_dict('list')\n\n    similar to:\n    ```\n    >>> import pandas as pd\n    >>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n    >>> df\n        a  b\n        0  1  4\n        1  2  5\n        2  3  6\n    >>> df.to_dict('list')\n    {'a': [1, 2, 3], 'b': [4, 5, 6]}\n    >>> t = Table.from_dict(df.to_dict('list))\n    >>> t.show()\n        +===+===+===+\n        | # | a | b |\n        |row|int|int|\n        +---+---+---+\n        | 0 |  1|  4|\n        | 1 |  2|  5|\n        | 2 |  3|  6|\n        +===+===+===+\n    ```\n    \"\"\"\n    return import_utils.from_pandas(cls, df)\n
"},{"location":"reference/core/#tablite.core.Table.from_hdf5","title":"tablite.core.Table.from_hdf5(path) classmethod","text":"

imports an exported hdf5 table.

Source code in tablite/core.py
@classmethod\ndef from_hdf5(cls, path):\n    \"\"\"\n    imports an exported hdf5 table.\n    \"\"\"\n    return import_utils.from_hdf5(cls, path)\n
"},{"location":"reference/core/#tablite.core.Table.from_json","title":"tablite.core.Table.from_json(jsn) classmethod","text":"

Imports table exported using .to_json

Source code in tablite/core.py
@classmethod\ndef from_json(cls, jsn):\n    \"\"\"\n    Imports table exported using .to_json\n    \"\"\"\n    return import_utils.from_json(cls, jsn)\n
"},{"location":"reference/core/#tablite.core.Table.to_hdf5","title":"tablite.core.Table.to_hdf5(path)","text":"

creates a copy of the table as hdf5

Source code in tablite/core.py
def to_hdf5(self, path):\n    \"\"\"\n    creates a copy of the table as hdf5\n    \"\"\"\n    export_utils.to_hdf5(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_pandas","title":"tablite.core.Table.to_pandas()","text":"

returns pandas.DataFrame

Source code in tablite/core.py
def to_pandas(self):\n    \"\"\"\n    returns pandas.DataFrame\n    \"\"\"\n    return export_utils.to_pandas(self)\n
"},{"location":"reference/core/#tablite.core.Table.to_sql","title":"tablite.core.Table.to_sql(name)","text":"

generates ANSI-92 compliant SQL.

Source code in tablite/core.py
def to_sql(self, name):\n    \"\"\"\n    generates ANSI-92 compliant SQL.\n    \"\"\"\n    return export_utils.to_sql(self, name)  # remove after update to test suite.\n
"},{"location":"reference/core/#tablite.core.Table.to_json","title":"tablite.core.Table.to_json()","text":"

returns JSON

Source code in tablite/core.py
def to_json(self):\n    \"\"\"\n    returns JSON\n    \"\"\"\n    return export_utils.to_json(self)\n
"},{"location":"reference/core/#tablite.core.Table.to_xlsx","title":"tablite.core.Table.to_xlsx(path)","text":"

exports table to path

Source code in tablite/core.py
def to_xlsx(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".xlsx\")\n    export_utils.excel_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_ods","title":"tablite.core.Table.to_ods(path)","text":"

exports table to path

Source code in tablite/core.py
def to_ods(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".ods\")\n    export_utils.excel_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_csv","title":"tablite.core.Table.to_csv(path)","text":"

exports table to path

Source code in tablite/core.py
def to_csv(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".csv\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_tsv","title":"tablite.core.Table.to_tsv(path)","text":"

exports table to path

Source code in tablite/core.py
def to_tsv(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".tsv\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_text","title":"tablite.core.Table.to_text(path)","text":"

exports table to path

Source code in tablite/core.py
def to_text(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".txt\")\n    export_utils.text_writer(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.to_html","title":"tablite.core.Table.to_html(path)","text":"

exports table to path

Source code in tablite/core.py
def to_html(self, path):\n    \"\"\"\n    exports table to path\n    \"\"\"\n    export_utils.path_suffix_check(path, \".html\")\n    export_utils.to_html(self, path)\n
"},{"location":"reference/core/#tablite.core.Table.expression","title":"tablite.core.Table.expression(expression)","text":"

filters based on an expression, such as:

\"all((A==B, C!=4, 200<D))\"\n

which is interpreted using python's compiler to:

def _f(A,B,C,D):\n    return all((A==B, C!=4, 200<D))\n
Source code in tablite/core.py
def expression(self, expression):\n    \"\"\"\n    filters based on an expression, such as:\n\n        \"all((A==B, C!=4, 200<D))\"\n\n    which is interpreted using python's compiler to:\n\n        def _f(A,B,C,D):\n            return all((A==B, C!=4, 200<D))\n    \"\"\"\n    return redux._filter_using_expression(self, expression)\n
"},{"location":"reference/core/#tablite.core.Table.filter","title":"tablite.core.Table.filter(expressions, filter_type='all', tqdm=_tqdm)","text":"

enables filtering across columns for multiple criteria.

expressions:

str: Expression that can be compiled and executed row by row.\n    exampLe: \"all((A==B and C!=4 and 200<D))\"\n\nlist of dicts: (example):\n\n    L = [\n        {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n        {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n        {'value1': 200, 'criteria': \"<\", column2: 'D' }\n    ]\n\naccepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n

filter_type: 'all' or 'any'

Source code in tablite/core.py
def filter(self, expressions, filter_type=\"all\", tqdm=_tqdm):\n    \"\"\"\n    enables filtering across columns for multiple criteria.\n\n    expressions:\n\n        str: Expression that can be compiled and executed row by row.\n            exampLe: \"all((A==B and C!=4 and 200<D))\"\n\n        list of dicts: (example):\n\n            L = [\n                {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n                {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n                {'value1': 200, 'criteria': \"<\", column2: 'D' }\n            ]\n\n        accepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n\n    filter_type: 'all' or 'any'\n    \"\"\"\n    return redux.filter(self, expressions, filter_type, tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.sort_index","title":"tablite.core.Table.sort_index(sort_mode='excel', tqdm=_tqdm, pbar=None, **kwargs)","text":"

helper for methods sort and is_sorted

param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default) param: **kwargs: sort criteria. See Table.sort()

Source code in tablite/core.py
def sort_index(self, sort_mode=\"excel\", tqdm=_tqdm, pbar=None, **kwargs):\n    \"\"\"\n    helper for methods `sort` and `is_sorted`\n\n    param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default)\n    param: **kwargs: sort criteria. See Table.sort()\n    \"\"\"\n    return sortation.sort_index(self, sort_mode, tqdm=tqdm, pbar=pbar, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.reindex","title":"tablite.core.Table.reindex(index)","text":"

index: list of integers that declare sort order.

Examples:

Table:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6]\nresult: ['b','d','f','h']\n\nTable:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6,1,3,5,7]\nresult: ['a','c','e','g','b','d','f','h']\n
Source code in tablite/core.py
def reindex(self, index):\n    \"\"\"\n    index: list of integers that declare sort order.\n\n    Examples:\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6]\n        result: ['b','d','f','h']\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6,1,3,5,7]\n        result: ['a','c','e','g','b','d','f','h']\n\n    \"\"\"\n    if isinstance(index, list):\n        index = np.array(index)\n    return _reindex.reindex(self, index)\n
"},{"location":"reference/core/#tablite.core.Table.drop_duplicates","title":"tablite.core.Table.drop_duplicates(*args)","text":"

removes duplicate rows based on column names

args: (optional) column_names if no args, all columns are used.

Source code in tablite/core.py
def drop_duplicates(self, *args):\n    \"\"\"\n    removes duplicate rows based on column names\n\n    args: (optional) column_names\n    if no args, all columns are used.\n    \"\"\"\n    if not args:\n        args = self.columns\n    index = self.unique_index(*args)\n    return self.reindex(index)\n
"},{"location":"reference/core/#tablite.core.Table.sort","title":"tablite.core.Table.sort(mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

Perform multi-pass sorting with precedence given order of column names.

PARAMETER DESCRIPTION mapping

keys as columns, values as boolean for 'reverse'

TYPE: dict

sort_mode

str: \"alphanumeric\", \"unix\", or, \"excel\"

DEFAULT: 'excel'

RETURNS DESCRIPTION None

Table.sort is sorted inplace

Examples: Table.sort(mappinp={A':False}) means sort by 'A' in ascending order. Table.sort(mapping={'A':True, 'B':False}) means sort 'A' in descending order, then (2nd priority) sort B in ascending order.

Source code in tablite/core.py
def sort(self, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"Perform multi-pass sorting with precedence given order of column names.\n\n    Args:\n        mapping (dict): keys as columns,\n                        values as boolean for 'reverse'\n        sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\"\n\n    Returns:\n        None: Table.sort is sorted inplace\n\n    Examples:\n    Table.sort(mappinp={A':False}) means sort by 'A' in ascending order.\n    Table.sort(mapping={'A':True, 'B':False}) means sort 'A' in descending order, then (2nd priority)\n    sort B in ascending order.\n    \"\"\"\n    new = sortation.sort(self, mapping, sort_mode, tqdm=tqdm, pbar=pbar)\n    self.columns = new.columns\n
"},{"location":"reference/core/#tablite.core.Table.sorted","title":"tablite.core.Table.sorted(mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

See sort. Sorted returns a new table in contrast to \"sort\", which is in-place.

RETURNS DESCRIPTION

Table.

Source code in tablite/core.py
def sorted(self, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"See sort.\n    Sorted returns a new table in contrast to \"sort\", which is in-place.\n\n    Returns:\n        Table.\n    \"\"\"\n    return sortation.sort(self, mapping, sort_mode, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.is_sorted","title":"tablite.core.Table.is_sorted(mapping, sort_mode='excel')","text":"

Performs multi-pass sorting check with precedence given order of column names. **kwargs: optional: sort criteria. See Table.sort() :return bool

Source code in tablite/core.py
def is_sorted(self, mapping, sort_mode=\"excel\"):\n    \"\"\"Performs multi-pass sorting check with precedence given order of column names.\n    **kwargs: optional: sort criteria. See Table.sort()\n    :return bool\n    \"\"\"\n    return sortation.is_sorted(self, mapping, sort_mode)\n
"},{"location":"reference/core/#tablite.core.Table.any","title":"tablite.core.Table.any(**kwargs)","text":"

returns Table for rows where ANY kwargs match :param kwargs: dictionary with headers and values / boolean callable

Source code in tablite/core.py
def any(self, **kwargs):\n    \"\"\"\n    returns Table for rows where ANY kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n    \"\"\"\n    return redux.filter_any(self, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.all","title":"tablite.core.Table.all(**kwargs)","text":"

returns Table for rows where ALL kwargs match :param kwargs: dictionary with headers and values / boolean callable

Examples:

t = Table()\nt['a'] = [1,2,3,4]\nt['b'] = [10,20,30,40]\n\ndef f(x):\n    return x == 4\ndef g(x):\n    return x < 20\n\nt2 = t.any( **{\"a\":f, \"b\":g})\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\nt2 = t.any(a=f,b=g)\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\ndef h(x):\n    return x>=2\n\ndef i(x):\n    return x<=30\n\nt2 = t.all(a=h,b=i)\nassert [r for r in t2.rows] == [[2,20], [3, 30]]\n
Source code in tablite/core.py
def all(self, **kwargs):\n    \"\"\"\n    returns Table for rows where ALL kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n\n    Examples:\n\n        t = Table()\n        t['a'] = [1,2,3,4]\n        t['b'] = [10,20,30,40]\n\n        def f(x):\n            return x == 4\n        def g(x):\n            return x < 20\n\n        t2 = t.any( **{\"a\":f, \"b\":g})\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        t2 = t.any(a=f,b=g)\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        def h(x):\n            return x>=2\n\n        def i(x):\n            return x<=30\n\n        t2 = t.all(a=h,b=i)\n        assert [r for r in t2.rows] == [[2,20], [3, 30]]\n\n\n    \"\"\"\n    return redux.filter_all(self, **kwargs)\n
"},{"location":"reference/core/#tablite.core.Table.drop","title":"tablite.core.Table.drop(*args)","text":"

removes all rows where args are present.

Exmaple:

t = Table() t['A'] = [1,2,3,None] t['B'] = [None,2,3,4] t2 = t.drop(None) t2'A', t2'B' ([2,3], [2,3])

Source code in tablite/core.py
def drop(self, *args):\n    \"\"\"\n    removes all rows where args are present.\n\n    Exmaple:\n    >>> t = Table()\n    >>> t['A'] = [1,2,3,None]\n    >>> t['B'] = [None,2,3,4]\n    >>> t2 = t.drop(None)\n    >>> t2['A'][:], t2['B'][:]\n    ([2,3], [2,3])\n\n    \"\"\"\n    if not args:\n        raise ValueError(\"What to drop? None? np.nan? \")\n    return redux.drop(self, *args)\n
"},{"location":"reference/core/#tablite.core.Table.replace","title":"tablite.core.Table.replace(mapping, columns=None, tqdm=_tqdm, pbar=None)","text":"

replaces all mapped keys with values from named columns

PARAMETER DESCRIPTION mapping

keys are targets for replacement, values are replacements.

TYPE: dict

columns

target columns. Defaults to None (all columns)

TYPE: list or str DEFAULT: None

RAISES DESCRIPTION ValueError

description

Source code in tablite/core.py
def replace(self, mapping, columns=None, tqdm=_tqdm, pbar=None):\n    \"\"\"replaces all mapped keys with values from named columns\n\n    Args:\n        mapping (dict): keys are targets for replacement,\n                        values are replacements.\n        columns (list or str, optional): target columns.\n            Defaults to None (all columns)\n\n    Raises:\n        ValueError: _description_\n    \"\"\"\n    if columns is None:\n        columns = list(self.columns)\n    if not isinstance(columns, list) and columns in self.columns:\n        columns = [columns]\n    type_check(columns, list)\n    for n in columns:\n        if n not in self.columns:\n            raise ValueError(f\"column not found: {n}\")\n\n    if pbar is None:\n        total = len(columns)\n        pbar = tqdm(total=total, desc=\"replace\", disable=Config.TQDM_DISABLE)\n\n    for name in columns:\n        col = self.columns[name]\n        col.replace(mapping)\n        pbar.update(1)\n
"},{"location":"reference/core/#tablite.core.Table.groupby","title":"tablite.core.Table.groupby(keys, functions, tqdm=_tqdm, pbar=None)","text":"

keys: column names for grouping. functions: [optional] list of column names and group functions (See GroupyBy class) returns: table

Example:

t = Table()\nt.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\nt.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\nt.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n\nt.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\ng = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\ng.show()\n+===+===+===+======+\n| # | A | C |Sum(B)|\n|row|int|int| int  |\n+---+---+---+------+\n|0  |  1|  6|     2|\n|1  |  1|  5|     4|\n|2  |  2|  4|     6|\n|3  |  2|  3|     8|\n|4  |  3|  2|    10|\n|5  |  3|  1|    12|\n+===+===+===+======+\n

Cheat sheet:

list of unique values

>>> g1 = t.groupby(keys=['A'], functions=[])\n>>> g1['A'][:]\n[1,2,3]\n

alternatively:

t['A'].unique() [1,2,3]

list of unique values, grouped by longest combination.

>>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n>>> g2['A'][:], g2['B'][:]\n([1,1,2,2,3,3], [1,2,3,4,5,6])\n

alternatively:

>>> list(zip(*t.index('A', 'B').keys()))\n[(1,1,2,2,3,3) (1,2,3,4,5,6)]\n

A key (unique values) and count hereof.

>>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n>>> g3['A'][:], g3['Count(A)'][:]\n([1,2,3], [4,4,4])\n

alternatively:

>>> t['A'].histogram()\n([1,2,3], [4,4,4])\n

for more exmaples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py

Source code in tablite/core.py
def groupby(self, keys, functions, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    keys: column names for grouping.\n    functions: [optional] list of column names and group functions (See GroupyBy class)\n    returns: table\n\n    Example:\n    ```\n    t = Table()\n    t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n    t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n    t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n\n    t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n    g.show()\n    +===+===+===+======+\n    | # | A | C |Sum(B)|\n    |row|int|int| int  |\n    +---+---+---+------+\n    |0  |  1|  6|     2|\n    |1  |  1|  5|     4|\n    |2  |  2|  4|     6|\n    |3  |  2|  3|     8|\n    |4  |  3|  2|    10|\n    |5  |  3|  1|    12|\n    +===+===+===+======+\n    ```\n    Cheat sheet:\n\n    list of unique values\n    ```\n    >>> g1 = t.groupby(keys=['A'], functions=[])\n    >>> g1['A'][:]\n    [1,2,3]\n    ```\n    alternatively:\n    >>> t['A'].unique()\n    [1,2,3]\n\n    list of unique values, grouped by longest combination.\n    ```\n    >>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n    >>> g2['A'][:], g2['B'][:]\n    ([1,1,2,2,3,3], [1,2,3,4,5,6])\n    ```\n    alternatively:\n    ```\n    >>> list(zip(*t.index('A', 'B').keys()))\n    [(1,1,2,2,3,3) (1,2,3,4,5,6)]\n    ```\n    A key (unique values) and count hereof.\n    ```\n    >>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n    >>> g3['A'][:], g3['Count(A)'][:]\n    ([1,2,3], [4,4,4])\n    ```\n    alternatively:\n    ```\n    >>> t['A'].histogram()\n    ([1,2,3], [4,4,4])\n    ```\n    for more exmaples see:\n        https://github.com/root-11/tablite/blob/master/tests/test_groupby.py\n\n    \"\"\"\n    return groupbys.groupby(self, keys, functions, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.pivot","title":"tablite.core.Table.pivot(rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None)","text":"

param: rows: column names to keep as rows param: columns: column names to keep as columns param: functions: aggregation functions from the Groupby class as

example:

t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\nt2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\nt2.show()\n+===+===+========+=====+=====+=====+\n| # | C |function|(A=1)|(A=2)|(A=3)|\n|row|int|  str   |mixed|mixed|mixed|\n+---+---+--------+-----+-----+-----+\n|0  |  6|Sum(B)  |    2|None |None |\n|1  |  5|Sum(B)  |    4|None |None |\n|2  |  4|Sum(B)  |None |    6|None |\n|3  |  3|Sum(B)  |None |    8|None |\n|4  |  2|Sum(B)  |None |None |   10|\n|5  |  1|Sum(B)  |None |None |   12|\n+===+===+========+=====+=====+=====+\n
Source code in tablite/core.py
def pivot(self, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    param: rows: column names to keep as rows\n    param: columns: column names to keep as columns\n    param: functions: aggregation functions from the Groupby class as\n\n    example:\n    ```\n    t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n    t2.show()\n    +===+===+========+=====+=====+=====+\n    | # | C |function|(A=1)|(A=2)|(A=3)|\n    |row|int|  str   |mixed|mixed|mixed|\n    +---+---+--------+-----+-----+-----+\n    |0  |  6|Sum(B)  |    2|None |None |\n    |1  |  5|Sum(B)  |    4|None |None |\n    |2  |  4|Sum(B)  |None |    6|None |\n    |3  |  3|Sum(B)  |None |    8|None |\n    |4  |  2|Sum(B)  |None |None |   10|\n    |5  |  1|Sum(B)  |None |None |   12|\n    +===+===+========+=====+=====+=====+\n    ```\n    \"\"\"\n    return pivots.pivot(self, rows, columns, functions, values_as_rows, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.merge","title":"tablite.core.Table.merge(left, right, new, criteria)","text":"

takes from LEFT where criteria is True else RIGHT. :param: T: Table :param: criteria: np.array(bool): if True take left column else take right column :param left: (str) column name :param right: (str) column name :param new: (str) new name

:returns: T

Example:

>>> c.show()\n+==+====+====+====+====+\n| #| A  | B  | C  | D  |\n+--+----+----+----+----+\n| 0|   1|  10|   1|  11|\n| 1|   2|  20|   2|  12|\n| 2|   3|None|   3|  13|\n| 3|None|  40|None|None|\n| 4|   5|  50|None|None|\n| 5|None|None|   6|  16|\n| 6|None|None|   7|  17|\n+==+====+====+====+====+\n\n>>> c.merge(\"A\", \"C\", new=\"E\", criteria=[v != None for v in c['A']])\n>>> c.show()\n+==+====+====+====+\n| #| B  | D  | E  |\n+--+----+----+----+\n| 0|  10|  11|   1|\n| 1|  20|  12|   2|\n| 2|None|  13|   3|\n| 3|  40|None|None|\n| 4|  50|None|   5|\n| 5|None|  16|   6|\n| 6|None|  17|   7|\n+==+====+====+====+\n
Source code in tablite/core.py
def merge(self, left, right, new, criteria):\n    \"\"\" takes from LEFT where criteria is True else RIGHT.\n    :param: T: Table\n    :param: criteria: np.array(bool): \n            if True take left column\n            else take right column\n    :param left: (str) column name\n    :param right: (str) column name\n    :param new: (str) new name\n\n    :returns: T\n\n    Example:\n    ```\n    >>> c.show()\n    +==+====+====+====+====+\n    | #| A  | B  | C  | D  |\n    +--+----+----+----+----+\n    | 0|   1|  10|   1|  11|\n    | 1|   2|  20|   2|  12|\n    | 2|   3|None|   3|  13|\n    | 3|None|  40|None|None|\n    | 4|   5|  50|None|None|\n    | 5|None|None|   6|  16|\n    | 6|None|None|   7|  17|\n    +==+====+====+====+====+\n\n    >>> c.merge(\"A\", \"C\", new=\"E\", criteria=[v != None for v in c['A']])\n    >>> c.show()\n    +==+====+====+====+\n    | #| B  | D  | E  |\n    +--+----+----+----+\n    | 0|  10|  11|   1|\n    | 1|  20|  12|   2|\n    | 2|None|  13|   3|\n    | 3|  40|None|None|\n    | 4|  50|None|   5|\n    | 5|None|  16|   6|\n    | 6|None|  17|   7|\n    +==+====+====+====+\n    ```\n    \"\"\"\n    return merge.where(self, criteria,left,right,new)\n
"},{"location":"reference/core/#tablite.core.Table.column_select","title":"tablite.core.Table.column_select(cols, tqdm=_tqdm, TaskManager=_TaskManager)","text":"

type-casts columns from a given table to specified type(s)

cols

list of dicts: (example):

cols = [\n    {'column':'A', 'type': 'bool'},\n    {'column':'B', 'type': 'int', 'allow_empty': True},\n    {'column':'B', 'type': 'float', 'allow_empty': False, 'rename': 'C'},\n]\n

'column' : column name of the input table that we want to type-cast 'type' : type that we want to type-cast the specified column to 'allow_empty': should we allow empty values (None, str('')) through (Default: False) 'rename' : new name of the column, if None will keep the original name, in case of duplicates suffix will be added (Default: None)

supported types: 'bool', 'int', 'float', 'str', 'date', 'time', 'datetime'

if any of the columns is rejected, entire row is rejected

tqdm: progressbar constructor TaskManager: TaskManager constructor

(TABLE, TABLE) DESCRIPTION

first table contains the rows that were successfully cast to desired types

second table contains rows that failed to cast + rejection reason

Source code in tablite/core.py
def column_select(self, cols, tqdm=_tqdm, TaskManager=_TaskManager):\n    \"\"\"\n    type-casts columns from a given table to specified type(s)\n\n    cols:\n        list of dicts: (example):\n\n            cols = [\n                {'column':'A', 'type': 'bool'},\n                {'column':'B', 'type': 'int', 'allow_empty': True},\n                {'column':'B', 'type': 'float', 'allow_empty': False, 'rename': 'C'},\n            ]\n\n        'column'     : column name of the input table that we want to type-cast\n        'type'       : type that we want to type-cast the specified column to\n        'allow_empty': should we allow empty values (None, str('')) through (Default: False)\n        'rename'     : new name of the column, if None will keep the original name, in case of duplicates suffix will be added (Default: None)\n\n        supported types: 'bool', 'int', 'float', 'str', 'date', 'time', 'datetime'\n\n        if any of the columns is rejected, entire row is rejected\n\n    tqdm: progressbar constructor\n    TaskManager: TaskManager constructor\n\n    returns: (Table, Table)\n        first table contains the rows that were successfully cast to desired types\n        second table contains rows that failed to cast + rejection reason\n    \"\"\"\n    return _column_select(self, cols, tqdm, TaskManager)\n
"},{"location":"reference/core/#tablite.core.Table.join","title":"tablite.core.Table.join(other, left_keys, right_keys, left_columns, right_columns, kind='inner', merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

short-cut for all join functions. kind: 'inner', 'left', 'outer', 'cross'

Source code in tablite/core.py
def join(self, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    short-cut for all join functions.\n    kind: 'inner', 'left', 'outer', 'cross'\n    \"\"\"\n    kinds = {\n        \"inner\": self.inner_join,\n        \"left\": self.left_join,\n        \"outer\": self.outer_join,\n        \"cross\": self.cross_join,\n    }\n    if kind not in kinds:\n        raise ValueError(f\"join type unknown: {kind}\")\n    f = kinds.get(kind, None)\n    return f(other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.left_join","title":"tablite.core.Table.left_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\nTablite: left_join = numbers.left_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n)\n
Source code in tablite/core.py
def left_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n    Tablite: left_join = numbers.left_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n    ```\n    \"\"\"\n    return joins.left_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.inner_join","title":"tablite.core.Table.inner_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\nTablite: inner_join = numbers.inner_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n
Source code in tablite/core.py
def inner_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    Tablite: inner_join = numbers.inner_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    ```\n    \"\"\"\n    return joins.inner_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.outer_join","title":"tablite.core.Table.outer_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

:param other: self, other = (left, right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :return: new Table Example:

SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\nTablite: outer_join = numbers.outer_join(\n    letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n    )\n
Source code in tablite/core.py
def outer_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param other: self, other = (left, right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :return: new Table\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    Tablite: outer_join = numbers.outer_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    ```\n    \"\"\"\n    return joins.outer_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.cross_join","title":"tablite.core.Table.cross_join(other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None)","text":"

CROSS JOIN returns the Cartesian product of rows from tables in the join. In other words, it will produce rows which combine each row from the first table with each row from the second table

Source code in tablite/core.py
def cross_join(self, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=False, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    CROSS JOIN returns the Cartesian product of rows from tables in the join.\n    In other words, it will produce rows which combine each row from the first table\n    with each row from the second table\n    \"\"\"\n    return joins.cross_join(self, other, left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/core/#tablite.core.Table.lookup","title":"tablite.core.Table.lookup(other, *criteria, all=True, tqdm=_tqdm)","text":"

function for looking up values in other according to criteria in ascending order. :param: other: Table sorted in ascending search order. :param: criteria: Each criteria must be a tuple with value comparisons in the form: (LEFT, OPERATOR, RIGHT) :param: all: boolean: True=ALL, False=Any

OPERATOR must be a callable that returns a boolean LEFT must be a value that the OPERATOR can compare. RIGHT must be a value that the OPERATOR can compare.

Examples:

('column A', \"==\", 'column B')  # comparison of two columns\n('Date', \"<\", DataTypes.date(24,12) )  # value from column 'Date' is before 24/12.\nf = lambda L,R: all( ord(L) < ord(R) )  # uses custom function.\n('text 1', f, 'text 2') value from column 'text 1' is compared with value from column 'text 2'\n
Source code in tablite/core.py
def lookup(self, other, *criteria, all=True, tqdm=_tqdm):\n    \"\"\"function for looking up values in `other` according to criteria in ascending order.\n    :param: other: Table sorted in ascending search order.\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n        (LEFT, OPERATOR, RIGHT)\n    :param: all: boolean: True=ALL, False=Any\n\n    OPERATOR must be a callable that returns a boolean\n    LEFT must be a value that the OPERATOR can compare.\n    RIGHT must be a value that the OPERATOR can compare.\n\n    Examples:\n    ```\n    ('column A', \"==\", 'column B')  # comparison of two columns\n    ('Date', \"<\", DataTypes.date(24,12) )  # value from column 'Date' is before 24/12.\n    f = lambda L,R: all( ord(L) < ord(R) )  # uses custom function.\n    ('text 1', f, 'text 2') value from column 'text 1' is compared with value from column 'text 2'\n    ```\n    \"\"\"\n    return lookup.lookup(self, other, *criteria, all=all, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.match","title":"tablite.core.Table.match(other, *criteria, keep_left=None, keep_right=None)","text":"

performs inner join where T matches other and removes rows that do not match.

:param: T: Table :param: other: Table :param: criteria: Each criteria must be a tuple with value comparisons in the form:

(LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\nExample:\n    ('column A', \"==\", 'column B')\n\nThis syntax follows the lookup syntax. See Lookup for details.\n

:param: keep_left: list of columns to keep. :param: keep_right: list of right columns to keep.

Source code in tablite/core.py
def match(self, other, *criteria, keep_left=None, keep_right=None):\n    \"\"\"\n    performs inner join where `T` matches `other` and removes rows that do not match.\n\n    :param: T: Table\n    :param: other: Table\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n\n        (LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\n        Example:\n            ('column A', \"==\", 'column B')\n\n        This syntax follows the lookup syntax. See Lookup for details.\n\n    :param: keep_left: list of columns to keep.\n    :param: keep_right: list of right columns to keep.\n    \"\"\"\n    return match.match(self, other, *criteria, keep_left=keep_left, keep_right=keep_right)\n
"},{"location":"reference/core/#tablite.core.Table.replace_missing_values","title":"tablite.core.Table.replace_missing_values(*args, **kwargs)","text":"Source code in tablite/core.py
def replace_missing_values(self, *args, **kwargs):\n    raise AttributeError(\"See imputation\")\n
"},{"location":"reference/core/#tablite.core.Table.imputation","title":"tablite.core.Table.imputation(targets, missing=None, method='carry forward', sources=None, tqdm=_tqdm)","text":"

In statistics, imputation is the process of replacing missing data with substituted values.

See more: https://en.wikipedia.org/wiki/Imputation_(statistics)

PARAMETER DESCRIPTION table

source table.

TYPE: Table

targets

column names to find and replace missing values

TYPE: str or list of strings

missing

values to be replaced.

TYPE: None or iterable DEFAULT: None

method

method to be used for replacement. Options:

'carry forward': takes the previous value, and carries forward into fields where values are missing. +: quick. Realistic on time series. -: Can produce strange outliers.

'mean': calculates the column mean (exclude missing) and copies the mean in as replacement. +: quick -: doesn't work on text. Causes data set to drift towards the mean.

'mode': calculates the column mode (exclude missing) and copies the mean in as replacement. +: quick -: most frequent value becomes over-represented in the sample

'nearest neighbour': calculates normalised distance between items in source columns selects nearest neighbour and copies value as replacement. +: works for any datatype. -: computationally intensive (e.g. slow)

TYPE: str DEFAULT: 'carry forward'

sources

NEAREST NEIGHBOUR ONLY column names to be used during imputation. if None or empty, all columns will be used.

TYPE: list of strings DEFAULT: None

RETURNS DESCRIPTION table

table with replaced values.

Source code in tablite/core.py
def imputation(self, targets, missing=None, method=\"carry forward\", sources=None, tqdm=_tqdm):\n    \"\"\"\n    In statistics, imputation is the process of replacing missing data with substituted values.\n\n    See more: https://en.wikipedia.org/wiki/Imputation_(statistics)\n\n    Args:\n        table (Table): source table.\n\n        targets (str or list of strings): column names to find and\n            replace missing values\n\n        missing (None or iterable): values to be replaced.\n\n        method (str): method to be used for replacement. Options:\n\n            'carry forward':\n                takes the previous value, and carries forward into fields\n                where values are missing.\n                +: quick. Realistic on time series.\n                -: Can produce strange outliers.\n\n            'mean':\n                calculates the column mean (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: doesn't work on text. Causes data set to drift towards the mean.\n\n            'mode':\n                calculates the column mode (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: most frequent value becomes over-represented in the sample\n\n            'nearest neighbour':\n                calculates normalised distance between items in source columns\n                selects nearest neighbour and copies value as replacement.\n                +: works for any datatype.\n                -: computationally intensive (e.g. slow)\n\n        sources (list of strings): NEAREST NEIGHBOUR ONLY\n            column names to be used during imputation.\n            if None or empty, all columns will be used.\n\n    Returns:\n        table: table with replaced values.\n    \"\"\"\n    return imputation.imputation(self, targets, missing, method, sources, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.transpose","title":"tablite.core.Table.transpose(tqdm=_tqdm)","text":"Source code in tablite/core.py
def transpose(self, tqdm=_tqdm):\n    return pivots.transpose(self, tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.pivot_transpose","title":"tablite.core.Table.pivot_transpose(columns, keep=None, column_name='transpose', value_name='value', tqdm=_tqdm)","text":"

Transpose a selection of columns to rows.

PARAMETER DESCRIPTION columns

column names to transpose

TYPE: list of column names

keep

column names to keep (repeat)

TYPE: list of column names DEFAULT: None

RETURNS DESCRIPTION Table

with columns transposed to rows

Example

transpose columns 1,2 and 3 and transpose the remaining columns, except sum.

Input:

| col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n|------|------|------|-----|-----|-----|-----|-----|------|\n| 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n| 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n| ...  |      |      |     |     |     |     |     |      |\n\nt.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\nOutput:\n\n|col1| col2| col3| transpose| value|\n|----|-----|-----|----------|------|\n|1234| 2345| 3456| sun      |   456|\n|1234| 2345| 3456| mon      |   567|\n|1244| 2445| 4456| mon      |     7|\n
Source code in tablite/core.py
def pivot_transpose(self, columns, keep=None, column_name=\"transpose\", value_name=\"value\", tqdm=_tqdm):\n    \"\"\"Transpose a selection of columns to rows.\n\n    Args:\n        columns (list of column names): column names to transpose\n        keep (list of column names): column names to keep (repeat)\n\n    Returns:\n        Table: with columns transposed to rows\n\n    Example:\n        transpose columns 1,2 and 3 and transpose the remaining columns, except `sum`.\n\n    Input:\n    ```\n    | col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n    |------|------|------|-----|-----|-----|-----|-----|------|\n    | 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n    | 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n    | ...  |      |      |     |     |     |     |     |      |\n\n    t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\n    Output:\n\n    |col1| col2| col3| transpose| value|\n    |----|-----|-----|----------|------|\n    |1234| 2345| 3456| sun      |   456|\n    |1234| 2345| 3456| mon      |   567|\n    |1244| 2445| 4456| mon      |     7|\n    ```\n    \"\"\"\n    return pivots.pivot_transpose(self, columns, keep, column_name, value_name, tqdm=tqdm)\n
"},{"location":"reference/core/#tablite.core.Table.diff","title":"tablite.core.Table.diff(other, columns=None)","text":"

compares table self with table other

PARAMETER DESCRIPTION self

Table

TYPE: Table

other

Table

TYPE: Table

columns

list of column names to include in comparison. Defaults to None.

TYPE: List DEFAULT: None

RETURNS DESCRIPTION Table

diff of self and other with diff in columns 1st and 2nd.

Source code in tablite/core.py
def diff(self, other, columns=None):\n    \"\"\"compares table self with table other\n\n    Args:\n        self (Table): Table\n        other (Table): Table\n        columns (List, optional): list of column names to include in comparison. Defaults to None.\n\n    Returns:\n        Table: diff of self and other with diff in columns 1st and 2nd.\n    \"\"\"\n    return diff.diff(self, other, columns)\n
"},{"location":"reference/core/#tablite.core-functions","title":"Functions","text":""},{"location":"reference/core/#tablite.core-modules","title":"Modules","text":""},{"location":"reference/datasets/","title":"Datasets","text":""},{"location":"reference/datasets/#tablite.datasets","title":"tablite.datasets","text":""},{"location":"reference/datasets/#tablite.datasets-classes","title":"Classes","text":""},{"location":"reference/datasets/#tablite.datasets-functions","title":"Functions","text":""},{"location":"reference/datasets/#tablite.datasets.synthetic_order_data","title":"tablite.datasets.synthetic_order_data(rows=100000)","text":"

Creates a synthetic dataset for testing that looks like this: (depending on number of rows)

+=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n|    ~    |   #   |      1      |         2         |  3  | 4 |  5  | 6  | 7 |  8  |  9  |         10        |        11        |\n|   row   |  int  |     int     |      datetime     | int |int| int |str |str|mixed|mixed|       float       |      float       |\n+---------+-------+-------------+-------------------+-----+---+-----+----+---+-----+-----+-------------------+------------------+\n|0        |      1|1478158906743|2021-10-27 00:00:00|50764|  1|29990|C4-5|APP|21\u00b0  |None | 2.0434376837650046|1.3371665497020444|\n|1        |      2|2271295805011|2021-09-13 00:00:00|50141|  0|10212|C4-5|TAE|None |None |  1.010318612835485| 20.94821610676901|\n|2        |      3|1598726492913|2021-08-19 00:00:00|50527|  0|19416|C3-5|QPV|21\u00b0  |None |  1.463459515469516|  17.4133659842749|\n|3        |      4|1413615572689|2021-11-05 00:00:00|50181|  1|18637|C4-2|GCL|6\u00b0   |ABC  |  2.084002469706324| 0.489481411683505|\n|4        |      5| 245266998048|2021-09-25 00:00:00|50378|  0|29756|C5-4|LGY|6\u00b0   |XYZ  | 0.5141579343276079| 8.550780816571438|\n|5        |      6| 947994853644|2021-10-14 00:00:00|50511|  0| 7890|C2-4|BET|0\u00b0   |XYZ  | 1.1725893606177542| 7.447314130260951|\n|6        |      7|2230693047809|2021-10-07 00:00:00|50987|  1|26742|C1-3|CFP|0\u00b0   |XYZ  | 1.0921267279498004|11.009210185311993|\n|...      |...    |...          |...                |...  |...|...  |... |...|...  |...  |...                |...               |\n|7,999,993|7999994|2047223556745|2021-09-03 00:00:00|50883|  1|15687|C3-1|RFR|None |XYZ  | 1.3467185981566827|17.023443485654845|\n|7,999,994|7999995|1814140654790|2021-08-02 00:00:00|50152|  0|16556|C4-2|WTC|None |ABC  | 1.1517593924478968| 8.201818634721487|\n|7,999,995|7999996| 155308171103|2021-10-14 00:00:00|50008|  1|14590|C1-3|WYM|0\u00b0   |None | 2.1273836233717978|23.295943554889195|\n|7,999,996|7999997|1620451532911|2021-12-12 00:00:00|50173|  1|20744|C2-1|ZYO|6\u00b0   |ABC  |  2.482509134693724| 22.25375464857266|\n|7,999,997|7999998|1248987682094|2021-12-20 00:00:00|50052|  1|28298|C5-4|XAW|None |XYZ  |0.17923757926558143|23.728160892974252|\n|7,999,998|7999999|1382206732187|2021-11-13 00:00:00|50993|  1|24832|C5-2|UDL|None |ABC  |0.08425329763360942|12.707735293126758|\n|7,999,999|8000000| 600688069780|2021-09-28 00:00:00|50510|  0|15819|C3-4|IGY|None |ABC  |  1.066241687256579|13.862069804070295|\n+=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n
PARAMETER DESCRIPTION rows

number of rows wanted. Defaults to 100_000.

TYPE: int DEFAULT: 100000

RETURNS DESCRIPTION Table

Populated table.

TYPE: Table

Source code in tablite/datasets.py
def synthetic_order_data(rows=100_000):\n    \"\"\"Creates a synthetic dataset for testing that looks like this:\n    (depending on number of rows)\n\n    ```\n    +=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n    |    ~    |   #   |      1      |         2         |  3  | 4 |  5  | 6  | 7 |  8  |  9  |         10        |        11        |\n    |   row   |  int  |     int     |      datetime     | int |int| int |str |str|mixed|mixed|       float       |      float       |\n    +---------+-------+-------------+-------------------+-----+---+-----+----+---+-----+-----+-------------------+------------------+\n    |0        |      1|1478158906743|2021-10-27 00:00:00|50764|  1|29990|C4-5|APP|21\u00b0  |None | 2.0434376837650046|1.3371665497020444|\n    |1        |      2|2271295805011|2021-09-13 00:00:00|50141|  0|10212|C4-5|TAE|None |None |  1.010318612835485| 20.94821610676901|\n    |2        |      3|1598726492913|2021-08-19 00:00:00|50527|  0|19416|C3-5|QPV|21\u00b0  |None |  1.463459515469516|  17.4133659842749|\n    |3        |      4|1413615572689|2021-11-05 00:00:00|50181|  1|18637|C4-2|GCL|6\u00b0   |ABC  |  2.084002469706324| 0.489481411683505|\n    |4        |      5| 245266998048|2021-09-25 00:00:00|50378|  0|29756|C5-4|LGY|6\u00b0   |XYZ  | 0.5141579343276079| 8.550780816571438|\n    |5        |      6| 947994853644|2021-10-14 00:00:00|50511|  0| 7890|C2-4|BET|0\u00b0   |XYZ  | 1.1725893606177542| 7.447314130260951|\n    |6        |      7|2230693047809|2021-10-07 00:00:00|50987|  1|26742|C1-3|CFP|0\u00b0   |XYZ  | 1.0921267279498004|11.009210185311993|\n    |...      |...    |...          |...                |...  |...|...  |... |...|...  |...  |...                |...               |\n    |7,999,993|7999994|2047223556745|2021-09-03 00:00:00|50883|  1|15687|C3-1|RFR|None |XYZ  | 1.3467185981566827|17.023443485654845|\n    |7,999,994|7999995|1814140654790|2021-08-02 00:00:00|50152|  0|16556|C4-2|WTC|None |ABC  | 1.1517593924478968| 8.201818634721487|\n    |7,999,995|7999996| 155308171103|2021-10-14 00:00:00|50008|  1|14590|C1-3|WYM|0\u00b0   |None | 2.1273836233717978|23.295943554889195|\n    |7,999,996|7999997|1620451532911|2021-12-12 00:00:00|50173|  1|20744|C2-1|ZYO|6\u00b0   |ABC  |  2.482509134693724| 22.25375464857266|\n    |7,999,997|7999998|1248987682094|2021-12-20 00:00:00|50052|  1|28298|C5-4|XAW|None |XYZ  |0.17923757926558143|23.728160892974252|\n    |7,999,998|7999999|1382206732187|2021-11-13 00:00:00|50993|  1|24832|C5-2|UDL|None |ABC  |0.08425329763360942|12.707735293126758|\n    |7,999,999|8000000| 600688069780|2021-09-28 00:00:00|50510|  0|15819|C3-4|IGY|None |ABC  |  1.066241687256579|13.862069804070295|\n    +=========+=======+=============+===================+=====+===+=====+====+===+=====+=====+===================+==================+\n    ```\n\n    Args:\n        rows (int, optional): number of rows wanted. Defaults to 100_000.\n\n    Returns:\n        Table (Table): Populated table.\n    \"\"\"  # noqa\n    rows = int(rows)\n\n    L1 = [\"None\", \"0\u00b0\", \"6\u00b0\", \"21\u00b0\"]\n    L2 = [\"ABC\", \"XYZ\", \"\"]\n\n    t = Table()\n    assert isinstance(t, Table)\n    for page_n in range(math.ceil(rows / Config.PAGE_SIZE)):  # n pages\n        start = (page_n * Config.PAGE_SIZE)\n        end = min(start + Config.PAGE_SIZE, rows)\n        ro = range(start, end)\n\n        t2 = Table()\n        t2[\"#\"] = [v+1 for v in ro]\n        # 1 - mock orderid\n        t2[\"1\"] = [random.randint(18_778_628_504, 2277_772_117_504) for i in ro]\n        # 2 - mock delivery date.\n        t2[\"2\"] = [datetime.fromordinal(random.randint(738000, 738150)).isoformat() for i in ro]\n        # 3 - mock store id.\n        t2[\"3\"] = [random.randint(50000, 51000) for _ in ro]\n        # 4 - random bit.\n        t2[\"4\"] = [random.randint(0, 1) for _ in ro]\n        # 5 - mock product id\n        t2[\"5\"] = [random.randint(3000, 30000) for _ in ro]\n        # 6 - random weird string\n        t2[\"6\"] = [f\"C{random.randint(1, 5)}-{random.randint(1, 5)}\" for _ in ro]\n        # 7 - # random category\n        t2[\"7\"] = [\"\".join(random.choice(ascii_uppercase) for _ in range(3)) for _ in ro]\n        # 8 -random temperature group.\n        t2[\"8\"] = [random.choice(L1) for _ in ro]\n        # 9 - random choice of category\n        t2[\"9\"] = [random.choice(L2) for _ in ro]\n        # 10 - volume?\n        t2[\"10\"] = [random.uniform(0.01, 2.5) for _ in ro]\n        # 11 - units?\n        t2[\"11\"] = [f\"{random.uniform(0.1, 25)}\" for _ in ro]\n\n        if len(t) == 0:\n            t = t2\n        else:\n            t += t2\n\n    return t\n
"},{"location":"reference/datatypes/","title":"Datatypes","text":""},{"location":"reference/datatypes/#tablite.datatypes","title":"tablite.datatypes","text":""},{"location":"reference/datatypes/#tablite.datatypes-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.matched_types","title":"tablite.datatypes.matched_types = {int: DataTypes._infer_int, str: DataTypes._infer_str, float: DataTypes._infer_float, bool: DataTypes._infer_bool, date: DataTypes._infer_date, datetime: DataTypes._infer_datetime, time: DataTypes._infer_time} module-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes-classes","title":"Classes","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes","title":"tablite.datatypes.DataTypes","text":"

Bases: object

DataTypes is the conversion library for all datatypes.

It supports any / all python datatypes.

"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.int","title":"tablite.datatypes.DataTypes.int = int class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.str","title":"tablite.datatypes.DataTypes.str = str class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.float","title":"tablite.datatypes.DataTypes.float = float class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.bool","title":"tablite.datatypes.DataTypes.bool = bool class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.date","title":"tablite.datatypes.DataTypes.date = date class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.datetime","title":"tablite.datatypes.DataTypes.datetime = datetime class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.time","title":"tablite.datatypes.DataTypes.time = time class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.timedelta","title":"tablite.datatypes.DataTypes.timedelta = timedelta class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.numeric_types","title":"tablite.datatypes.DataTypes.numeric_types = {int, float, date, time, datetime} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.epoch","title":"tablite.datatypes.DataTypes.epoch = datetime(2000, 1, 1, 0, 0, 0, 0, timezone.utc) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.epoch_no_tz","title":"tablite.datatypes.DataTypes.epoch_no_tz = datetime(2000, 1, 1, 0, 0, 0, 0) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.digits","title":"tablite.datatypes.DataTypes.digits = '1234567890' class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.decimals","title":"tablite.datatypes.DataTypes.decimals = set('1234567890-+eE.') class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.integers","title":"tablite.datatypes.DataTypes.integers = set('1234567890-+') class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.nones","title":"tablite.datatypes.DataTypes.nones = {'null', 'Null', 'NULL', '#N/A', '#n/a', '', 'None', None, np.nan} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.none_type","title":"tablite.datatypes.DataTypes.none_type = type(None) class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.bytes_functions","title":"tablite.datatypes.DataTypes.bytes_functions = {type(None): b_none, bool: b_bool, int: b_int, float: b_float, str: b_str, bytes: b_bytes, datetime: b_datetime, date: b_date, time: b_time, timedelta: b_timedelta} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.type_code_functions","title":"tablite.datatypes.DataTypes.type_code_functions = {1: _none, 2: _bool, 3: _int, 4: _float, 5: _str, 6: _bytes, 7: _datetime, 8: _date, 9: _time, 10: _timedelta, 11: _unpickle} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.pytype_from_type_code","title":"tablite.datatypes.DataTypes.pytype_from_type_code = {1: type(None), 2: bool, 3: int, 4: float, 5: str, 6: bytes, 7: datetime, 8: date, 9: time, 10: timedelta, 11: 'pickled object'} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.date_formats","title":"tablite.datatypes.DataTypes.date_formats = {'NNNN-NN-NN': lambda : date(*int(i) for i in x.split('-')), 'NNNN-N-NN': lambda : date(*int(i) for i in x.split('-')), 'NNNN-NN-N': lambda : date(*int(i) for i in x.split('-')), 'NNNN-N-N': lambda : date(*int(i) for i in x.split('-')), 'NN-NN-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'N-NN-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'NN-N-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'N-N-NNNN': lambda : date(*[int(i) for i in x.split('-')][::-1]), 'NNNN.NN.NN': lambda : date(*int(i) for i in x.split('.')), 'NNNN.N.NN': lambda : date(*int(i) for i in x.split('.')), 'NNNN.NN.N': lambda : date(*int(i) for i in x.split('.')), 'NNNN.N.N': lambda : date(*int(i) for i in x.split('.')), 'NN.NN.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'N.NN.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'NN.N.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'N.N.NNNN': lambda : date(*[int(i) for i in x.split('.')][::-1]), 'NNNN/NN/NN': lambda : date(*int(i) for i in x.split('/')), 'NNNN/N/NN': lambda : date(*int(i) for i in x.split('/')), 'NNNN/NN/N': lambda : date(*int(i) for i in x.split('/')), 'NNNN/N/N': lambda : date(*int(i) for i in x.split('/')), 'NN/NN/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'N/NN/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'NN/N/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'N/N/NNNN': lambda : date(*[int(i) for i in x.split('/')][::-1]), 'NNNN NN NN': lambda : date(*int(i) for i in x.split(' ')), 'NNNN N NN': lambda : date(*int(i) for i in x.split(' ')), 'NNNN NN N': lambda : date(*int(i) for i in x.split(' ')), 'NNNN N N': lambda : date(*int(i) for i in x.split(' ')), 'NN NN NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'N N NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'NN N NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'N NN NNNN': lambda : date(*[int(i) for i in x.split(' ')][::-1]), 'NNNNNNNN': lambda : date(*(int(x[:4]), int(x[4:6]), int(x[6:])))} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.datetime_formats","title":"tablite.datatypes.DataTypes.datetime_formats = {'NNNN-NN-NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x), 'NNNN-NN-NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x), 'NNNN-NN-NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, T=' '), 'NNNN-NN-NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, T=' '), 'NNNN/NN/NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN/NN/NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN NN NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN NN NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN.NN.NNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NNNN.NN.NN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NN-NN-NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN/NN/NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN/NN/NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN NN NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN.NN.NNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNNTNN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN': lambda : DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NNNNNNNNTNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNN': lambda : DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNNNN': lambda : DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNTNN:NN:NN': lambda : DataTypes.pattern_to_datetime(x, compact=3)} class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.types","title":"tablite.datatypes.DataTypes.types = [datetime, date, time, int, bool, float, str] class-attribute instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.type_code","title":"tablite.datatypes.DataTypes.type_code(value) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef type_code(cls, value):\n    if type(value) in cls._type_codes:\n        return cls._type_codes[type(value)]\n    elif hasattr(value, \"dtype\"):\n        dtype = pytype(value)\n        return cls._type_codes[dtype]\n    else:\n        return cls._type_codes[\"pickle\"]\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_none","title":"tablite.datatypes.DataTypes.b_none(v)","text":"Source code in tablite/datatypes.py
def b_none(v):\n    return b\"None\"\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_bool","title":"tablite.datatypes.DataTypes.b_bool(v)","text":"Source code in tablite/datatypes.py
def b_bool(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_int","title":"tablite.datatypes.DataTypes.b_int(v)","text":"Source code in tablite/datatypes.py
def b_int(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_float","title":"tablite.datatypes.DataTypes.b_float(v)","text":"Source code in tablite/datatypes.py
def b_float(v):\n    return bytes(str(v), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_str","title":"tablite.datatypes.DataTypes.b_str(v)","text":"Source code in tablite/datatypes.py
def b_str(v):\n    return v.encode(\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_bytes","title":"tablite.datatypes.DataTypes.b_bytes(v)","text":"Source code in tablite/datatypes.py
def b_bytes(v):\n    return v\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_datetime","title":"tablite.datatypes.DataTypes.b_datetime(v)","text":"Source code in tablite/datatypes.py
def b_datetime(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_date","title":"tablite.datatypes.DataTypes.b_date(v)","text":"Source code in tablite/datatypes.py
def b_date(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_time","title":"tablite.datatypes.DataTypes.b_time(v)","text":"Source code in tablite/datatypes.py
def b_time(v):\n    return bytes(v.isoformat(), encoding=\"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_timedelta","title":"tablite.datatypes.DataTypes.b_timedelta(v)","text":"Source code in tablite/datatypes.py
def b_timedelta(v):\n    return bytes(str(float(v.days + (v.seconds / (24 * 60 * 60)))), \"utf-8\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.b_pickle","title":"tablite.datatypes.DataTypes.b_pickle(v)","text":"Source code in tablite/datatypes.py
def b_pickle(v):\n    return pickle.dumps(v, protocol=0)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.to_bytes","title":"tablite.datatypes.DataTypes.to_bytes(v) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef to_bytes(cls, v):\n    if type(v) in cls.bytes_functions:  # it's a python native type\n        f = cls.bytes_functions[type(v)]\n    elif hasattr(v, \"dtype\"):  # it's a numpy/c type.\n        dtype = pytype(v)\n        f = cls.bytes_functions[dtype]\n    else:\n        f = cls.b_pickle\n    return f(v)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.from_type_code","title":"tablite.datatypes.DataTypes.from_type_code(value, code) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef from_type_code(cls, value, code):\n    f = cls.type_code_functions[code]\n    return f(value)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.pattern_to_datetime","title":"tablite.datatypes.DataTypes.pattern_to_datetime(iso_string, ymd=None, T=None, compact=0, day_first=False) staticmethod","text":"Source code in tablite/datatypes.py
@staticmethod\ndef pattern_to_datetime(iso_string, ymd=None, T=None, compact=0, day_first=False):\n    assert isinstance(iso_string, str)\n    if compact:\n        s = iso_string\n        if compact == 1:  # has T\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (9, 11, \":\"),\n                (11, 13, \":\"),\n                (13, len(s), \"\"),\n            ]\n        elif compact == 2:  # has no T.\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (8, 10, \":\"),\n                (10, 12, \":\"),\n                (12, len(s), \"\"),\n            ]\n        elif compact == 3:  # has T and :\n            slices = [\n                (0, 4, \"-\"),\n                (4, 6, \"-\"),\n                (6, 8, \"T\"),\n                (9, 11, \":\"),\n                (12, 14, \":\"),\n                (15, len(s), \"\"),\n            ]\n        else:\n            raise TypeError\n        iso_string = \"\".join([s[a:b] + c for a, b, c in slices if b <= len(s)])\n        iso_string = iso_string.rstrip(\":\")\n\n    if day_first:\n        s = iso_string\n        iso_string = \"\".join((s[6:10], \"-\", s[3:5], \"-\", s[0:2], s[10:]))\n\n    if \",\" in iso_string:\n        iso_string = iso_string.replace(\",\", \".\")\n\n    dot = iso_string[::-1].find(\".\")\n    if 0 < dot < 10:\n        ix = len(iso_string) - dot\n        microsecond = int(float(f\"0{iso_string[ix - 1:]}\") * 10**6)\n        # fmt:off\n        iso_string = iso_string[: len(iso_string) - dot] + str(microsecond).rjust(6, \"0\")\n        # fmt:on\n    if ymd:\n        iso_string = iso_string.replace(ymd, \"-\", 2)\n    if T:\n        iso_string = iso_string.replace(T, \"T\")\n    return datetime.fromisoformat(iso_string)\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.round","title":"tablite.datatypes.DataTypes.round(value, multiple, up=None) classmethod","text":"

a nicer way to round numbers.

PARAMETER DESCRIPTION value

value to be rounded

TYPE: (float, integer, datetime)

multiple

value to be used as the based of rounding. 1) multiple = 1 is the same as rounding to whole integers. 2) multiple = 0.001 is the same as rounding to 3 digits precision. 3) mulitple = 3.1415 is rounding to nearest multiplier of 3.1415 4) value = datetime(2022,8,18,11,14,53,440) 5) multiple = timedelta(hours=0.5) 6) xround(value,multiple) is datetime(2022,8,18,11,0)

TYPE: (float, integer, timedelta)

up

None (default) or boolean rounds half, up or down. round(1.6, 1) rounds to 2. round(1.4, 1) rounds to 1. round(1.5, 1, up=True) rounds to 2. round(1.5, 1, up=False) rounds to 1.

TYPE: (None, bool) DEFAULT: None

RETURNS DESCRIPTION

float,integer,datetime: rounded value in same type as input.

Source code in tablite/datatypes.py
@classmethod\ndef round(cls, value, multiple, up=None):\n    \"\"\"a nicer way to round numbers.\n\n    Args:\n        value (float,integer,datetime): value to be rounded\n\n        multiple (float,integer,timedelta): value to be used as the based of rounding.\n            1) multiple = 1 is the same as rounding to whole integers.\n            2) multiple = 0.001 is the same as rounding to 3 digits precision.\n            3) mulitple = 3.1415 is rounding to nearest multiplier of 3.1415\n            4) value = datetime(2022,8,18,11,14,53,440)\n            5) multiple = timedelta(hours=0.5)\n            6) xround(value,multiple) is datetime(2022,8,18,11,0)\n\n        up (None, bool, optional):\n            None (default) or boolean rounds half, up or down.\n            round(1.6, 1) rounds to 2.\n            round(1.4, 1) rounds to 1.\n            round(1.5, 1, up=True) rounds to 2.\n            round(1.5, 1, up=False) rounds to 1.\n\n    Returns:\n        float,integer,datetime: rounded value in same type as input.\n    \"\"\"\n    epoch = 0\n    if isinstance(value, (datetime)) and isinstance(multiple, timedelta):\n        if value.tzinfo is None:\n            epoch = cls.epoch_no_tz\n        else:\n            epoch = cls.epoch\n\n    value2 = value - epoch\n    if value2 == 0:\n        return value2\n\n    low = (value2 // multiple) * multiple\n    high = low + multiple\n    if up is True:\n        return high + epoch\n    elif up is False:\n        return low + epoch\n    else:\n        if abs((high + epoch) - value) < abs(value - (low + epoch)):\n            return high + epoch\n        else:\n            return low + epoch\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.to_json","title":"tablite.datatypes.DataTypes.to_json(v) staticmethod","text":"

converts any python type to json.

PARAMETER DESCRIPTION v

value to convert to json

TYPE: any

RETURNS DESCRIPTION

json compatible value from v

Source code in tablite/datatypes.py
@staticmethod\ndef to_json(v):\n    \"\"\"converts any python type to json.\n\n    Args:\n        v (any): value to convert to json\n\n    Returns:\n        json compatible value from v\n    \"\"\"\n    if hasattr(v, \"dtype\"):\n        v = numpy_to_python(v)\n    if v is None:\n        return v\n    elif v is False:\n        # using isinstance(v, bool): won't work as False also is int of zero.\n        return str(v)\n    elif v is True:\n        return str(v)\n    elif isinstance(v, int):\n        return v\n    elif isinstance(v, str):\n        return v\n    elif isinstance(v, float):\n        return v\n    elif isinstance(v, datetime):\n        return v.isoformat()\n    elif isinstance(v, time):\n        return v.isoformat()\n    elif isinstance(v, date):\n        return v.isoformat()\n    elif isinstance(v, timedelta):\n        return f\"P{v.days}DT{v.seconds + (v.microseconds / 1e6)}S\"\n    else:\n        raise TypeError(f\"The datatype {type(v)} is not supported.\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.from_json","title":"tablite.datatypes.DataTypes.from_json(v, dtype) staticmethod","text":"

converts json to python datatype

PARAMETER DESCRIPTION v

value

TYPE: any

dtype

any python type

TYPE: python type

RETURNS DESCRIPTION

python type of value v

Source code in tablite/datatypes.py
@staticmethod\ndef from_json(v, dtype):\n    \"\"\"converts json to python datatype\n\n    Args:\n        v (any): value\n        dtype (python type): any python type\n\n    Returns:\n        python type of value v\n    \"\"\"\n    if v in DataTypes.nones:\n        if dtype is str and v == \"\":\n            return \"\"\n        else:\n            return None\n    if dtype is int:\n        return int(v)\n    elif dtype is str:\n        return str(v)\n    elif dtype is float:\n        return float(v)\n    elif dtype is bool:\n        if v == \"False\":\n            return False\n        elif v == \"True\":\n            return True\n        else:\n            raise ValueError(v)\n    elif dtype is date:\n        return date.fromisoformat(v)\n    elif dtype is datetime:\n        return datetime.fromisoformat(v)\n    elif dtype is time:\n        return time.fromisoformat(v)\n    elif dtype is timedelta:\n        L = v.split(\"DT\")\n        days = int(L[0].lstrip(\"P\"))\n        seconds = float(L[1].rstrip(\"S\"))\n        return timedelta(days, seconds)\n    else:\n        raise TypeError(f\"The datatype {str(dtype)} is not supported.\")\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.guess_types","title":"tablite.datatypes.DataTypes.guess_types(*values) staticmethod","text":"

Attempts to guess the datatype for *values returns dict with matching datatypes and probabilities

RETURNS DESCRIPTION dict

{key: type, value: probability}

Source code in tablite/datatypes.py
@staticmethod\ndef guess_types(*values):\n    \"\"\"Attempts to guess the datatype for *values\n    returns dict with matching datatypes and probabilities\n\n    Returns:\n        dict: {key: type, value: probability}\n    \"\"\"\n    d = defaultdict(int)\n    probability = Rank(DataTypes.types[:])\n\n    for value in values:\n        if hasattr(value, \"dtype\"):\n            value = numpy_to_python(value)\n\n        for dtype in probability:\n            try:\n                _ = DataTypes.infer(value, dtype)\n                d[dtype] += 1\n                probability.match(dtype)\n                break\n            except (ValueError, TypeError):\n                pass\n    if not d:\n        d[str] = len(values)\n    return {k: round(v / len(values), 3) for k, v in d.items()}\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.guess","title":"tablite.datatypes.DataTypes.guess(*values) staticmethod","text":"

Makes a best guess the datatype for *values returns list of native python values

RETURNS DESCRIPTION list

list of native python values

Source code in tablite/datatypes.py
@staticmethod\ndef guess(*values):\n    \"\"\"Makes a best guess the datatype for *values\n    returns list of native python values\n\n    Returns:\n        list: list of native python values\n    \"\"\"\n    probability = Rank(*DataTypes.types[:])\n    matches = [None for _ in values[0]]\n\n    for ix, value in enumerate(values[0]):\n        if hasattr(value, \"dtype\"):\n            value = numpy_to_python(value)\n        for dtype in probability:\n            try:\n                matches[ix] = DataTypes.infer(value, dtype)\n                probability.match(dtype)\n                break\n            except (ValueError, TypeError):\n                pass\n    return matches\n
"},{"location":"reference/datatypes/#tablite.datatypes.DataTypes.infer","title":"tablite.datatypes.DataTypes.infer(v, dtype) classmethod","text":"Source code in tablite/datatypes.py
@classmethod\ndef infer(cls, v, dtype):\n    if isinstance(v, str) and dtype == str:\n        # we got a string, we're trying to infer it to string, we shouldn't check for None-ness\n        return v\n\n    if v in DataTypes.nones:\n        return None\n\n    if dtype not in matched_types:\n        raise TypeError(f\"The datatype {str(dtype)} is not supported.\")\n\n    return matched_types[dtype](v)\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank","title":"tablite.datatypes.Rank(*items)","text":"

Bases: object

Source code in tablite/datatypes.py
def __init__(self, *items):\n    self.items = {i: ix for i, ix in zip(items, range(len(items)))}\n    self.ranks = [0 for _ in items]\n    self.items_list = [i for i in items]\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank-attributes","title":"Attributes","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.items","title":"tablite.datatypes.Rank.items = {i: ixfor (i, ix) in zip(items, range(len(items)))} instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.ranks","title":"tablite.datatypes.Rank.ranks = [0 for _ in items] instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.items_list","title":"tablite.datatypes.Rank.items_list = [i for i in items] instance-attribute","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.Rank.match","title":"tablite.datatypes.Rank.match(k)","text":"Source code in tablite/datatypes.py
def match(self, k):  # k+=1\n    ix = self.items[k]\n    r = self.ranks\n    r[ix] += 1\n\n    if ix > 0:\n        p = self.items_list\n        while (\n            r[ix] > r[ix - 1] and ix > 0\n        ):  # use a simple bubble sort to maintain rank\n            r[ix], r[ix - 1] = r[ix - 1], r[ix]\n            p[ix], p[ix - 1] = p[ix - 1], p[ix]\n            old = p[ix]\n            self.items[old] = ix\n            self.items[k] = ix - 1\n            ix -= 1\n
"},{"location":"reference/datatypes/#tablite.datatypes.Rank.__iter__","title":"tablite.datatypes.Rank.__iter__()","text":"Source code in tablite/datatypes.py
def __iter__(self):\n    return iter(self.items_list)\n
"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray","title":"tablite.datatypes.MetaArray","text":"

Bases: ndarray

Array with metadata.

"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.MetaArray.__new__","title":"tablite.datatypes.MetaArray.__new__(array, dtype=None, order=None, **kwargs)","text":"Source code in tablite/datatypes.py
def __new__(cls, array, dtype=None, order=None, **kwargs):\n    obj = np.asarray(array, dtype=dtype, order=order).view(cls)\n    obj.metadata = kwargs\n    return obj\n
"},{"location":"reference/datatypes/#tablite.datatypes.MetaArray.__array_finalize__","title":"tablite.datatypes.MetaArray.__array_finalize__(obj)","text":"Source code in tablite/datatypes.py
def __array_finalize__(self, obj):\n    if obj is None:\n        return\n    self.metadata = getattr(obj, \"metadata\", None)\n
"},{"location":"reference/datatypes/#tablite.datatypes-functions","title":"Functions","text":""},{"location":"reference/datatypes/#tablite.datatypes.numpy_to_python","title":"tablite.datatypes.numpy_to_python(obj: Any) -> Any","text":"

Converts numpy types to python types.

See https://numpy.org/doc/stable/reference/arrays.scalars.html

PARAMETER DESCRIPTION obj

A numpy object

TYPE: Any

RETURNS DESCRIPTION Any

python object: A python object

Source code in tablite/datatypes.py
def numpy_to_python(obj: Any) -> Any:\n    \"\"\"Converts numpy types to python types.\n\n    See https://numpy.org/doc/stable/reference/arrays.scalars.html\n\n    Args:\n        obj (Any): A numpy object\n\n    Returns:\n        python object: A python object\n    \"\"\"\n    if isinstance(obj, np.generic):\n        return obj.item()\n    return obj\n
"},{"location":"reference/datatypes/#tablite.datatypes.pytype","title":"tablite.datatypes.pytype(obj)","text":"

Returns the python type of any object

PARAMETER DESCRIPTION obj

any numpy or python object

TYPE: Any

RETURNS DESCRIPTION type

type of obj

Source code in tablite/datatypes.py
def pytype(obj):\n    \"\"\"Returns the python type of any object\n\n    Args:\n        obj (Any): any numpy or python object\n\n    Returns:\n        type: type of obj\n    \"\"\"\n    if isinstance(obj, np.generic):\n        return type(obj.item())\n    return type(obj)\n
"},{"location":"reference/datatypes/#tablite.datatypes.pytype_from_iterable","title":"tablite.datatypes.pytype_from_iterable(iterable: {tuple, list}) -> {np.dtype, dict}","text":"

helper to make correct np array from python types.

PARAMETER DESCRIPTION iterable

values to be converted to numpy array.

TYPE: (tuple, list)

RAISES DESCRIPTION NotImplementedError

if datatype is not supported.

RETURNS DESCRIPTION {dtype, dict}

np.dtype: python type of the iterable.

Source code in tablite/datatypes.py
def pytype_from_iterable(iterable: {tuple, list}) -> {np.dtype, dict}:\n    \"\"\"helper to make correct np array from python types.\n\n    Args:\n        iterable (tuple,list): values to be converted to numpy array.\n\n    Raises:\n        NotImplementedError: if datatype is not supported.\n\n    Returns:\n        np.dtype: python type of the iterable.\n    \"\"\"\n    py_types = {}\n    if isinstance(iterable, (tuple, list)):\n        type_counter = Counter((pytype(v) for v in iterable))\n\n        for k, v in type_counter.items():\n            py_types[k] = v\n\n        if len(py_types) == 0:\n            np_dtype, py_dtype = object, bool\n        elif len(py_types) == 1:\n            py_dtype = list(py_types.keys())[0]\n            if py_dtype == datetime:\n                np_dtype = np.datetime64\n            elif py_dtype == date:\n                np_dtype = np.datetime64\n            elif py_dtype == timedelta:\n                np_dtype = np.timedelta64\n            else:\n                np_dtype = None\n        else:\n            np_dtype = object\n    elif isinstance(iterable, np.ndarray):\n        if iterable.dtype == object:\n            np_dtype = object\n            py_types = dict(Counter((pytype(v) for v in iterable)))\n        else:\n            np_dtype = iterable.dtype\n            if len(iterable) > 0:\n                py_types = {pytype(iterable[0]): len(iterable)}\n            else:\n                py_types = {pytype(np_dtype.type()): len(iterable)}\n    else:\n        raise NotImplementedError(f\"No handler for {type(iterable)}\")\n\n    return np_dtype, py_types\n
"},{"location":"reference/datatypes/#tablite.datatypes.list_to_np_array","title":"tablite.datatypes.list_to_np_array(iterable)","text":"

helper to make correct np array from python types. Example of problem where numpy turns mixed types into strings.

np.array([4, '5']) np.ndarray(['4', '5'])

RETURNS DESCRIPTION

np.array

datatypes

Source code in tablite/datatypes.py
def list_to_np_array(iterable):\n    \"\"\"helper to make correct np array from python types.\n    Example of problem where numpy turns mixed types into strings.\n    >>> np.array([4, '5'])\n    np.ndarray(['4', '5'])\n\n    returns:\n        np.array\n        datatypes\n    \"\"\"\n    np_dtype, py_dtype = pytype_from_iterable(iterable)\n\n    value = MetaArray(iterable, dtype=np_dtype, py_dtype=py_dtype)\n    return value\n
"},{"location":"reference/datatypes/#tablite.datatypes.np_type_unify","title":"tablite.datatypes.np_type_unify(arrays)","text":"

unifies numpy types.

PARAMETER DESCRIPTION arrays

List of numpy arrays

TYPE: list

RETURNS DESCRIPTION

np.ndarray: numpy array of a single type.

Source code in tablite/datatypes.py
def np_type_unify(arrays):\n    \"\"\"unifies numpy types.\n\n    Args:\n        arrays (list): List of numpy arrays\n\n    Returns:\n        np.ndarray: numpy array of a single type.\n    \"\"\"\n    dtypes = {arr.dtype: len(arr) for arr in arrays}\n    if len(dtypes) == 1:\n        dtype, _ = dtypes.popitem()\n    else:\n        for ix, arr in enumerate(arrays):\n            arrays[ix] = np.array(arr, dtype=object)\n        dtype = object\n    return np.concatenate(arrays, dtype=dtype)\n
"},{"location":"reference/datatypes/#tablite.datatypes.multitype_set","title":"tablite.datatypes.multitype_set(arr)","text":"

prevents loss of True, False when calling sets.

python looses values when called returning a set. Example:

{1, True, 0, False}

PARAMETER DESCRIPTION arr

iterable of mixed types.

TYPE: Iterable

RETURNS DESCRIPTION

np.array: with unique values.

Source code in tablite/datatypes.py
def multitype_set(arr):\n    \"\"\"prevents loss of True, False when calling sets.\n\n    python looses values when called returning a set. Example:\n    >>> {1, True, 0, False}\n    {0,1}\n\n    Args:\n        arr (Iterable): iterable of mixed types.\n\n    Returns:\n        np.array: with unique values.\n    \"\"\"\n    L = [(type(v), v) for v in arr]\n    L = list(set(L))\n    L = [v for _, v in L]\n    return np.array(L, dtype=object)\n
"},{"location":"reference/diff/","title":"Diff","text":""},{"location":"reference/diff/#tablite.diff","title":"tablite.diff","text":""},{"location":"reference/diff/#tablite.diff-classes","title":"Classes","text":""},{"location":"reference/diff/#tablite.diff-functions","title":"Functions","text":""},{"location":"reference/diff/#tablite.diff.diff","title":"tablite.diff.diff(T, other, columns=None)","text":"

compares table self with table other

PARAMETER DESCRIPTION self

Table

TYPE: Table

other

Table

TYPE: Table

columns

list of column names to include in comparison. Defaults to None.

TYPE: List DEFAULT: None

RETURNS DESCRIPTION Table

diff of self and other with diff in columns 1st and 2nd.

Source code in tablite/diff.py
def diff(T, other, columns=None):\n    \"\"\"compares table self with table other\n\n    Args:\n        self (Table): Table\n        other (Table): Table\n        columns (List, optional): list of column names to include in comparison. Defaults to None.\n\n    Returns:\n        Table: diff of self and other with diff in columns 1st and 2nd.\n    \"\"\"\n    sub_cls_check(T, Table)\n    sub_cls_check(other, Table)\n    if columns is None:\n        columns = [name for name in T.columns if name in other.columns]\n    elif isinstance(columns, list) and all(isinstance(i, str) for i in columns):\n        for name in columns:\n            if name not in T.columns:\n                raise ValueError(f\"column '{name}' not found\")\n            if name not in other.columns:\n                raise ValueError(f\"column '{name}' not found\")\n    else:\n        raise TypeError(\"Expected list of column names\")\n\n    t1 = T[columns]\n    if issubclass(type(t1), Table):\n        t1 = [tuple(r) for r in T.rows]\n    else:\n        t1 = list(T)\n    t2 = other[columns]\n    if issubclass(type(t2), Table):\n        t2 = [tuple(r) for r in other.rows]\n    else:\n        t2 = list(other)\n\n    sm = difflib.SequenceMatcher(None, t1, t2)\n    new = type(T)()\n    first = unique_name(\"1st\", columns)\n    second = unique_name(\"2nd\", columns)\n    new.add_columns(*columns + [first, second])\n\n    news = {n: [] for n in new.columns}  # Cache for Work in progress.\n\n    for opc, t1a, t1b, t2a, t2b in sm.get_opcodes():\n        if opc == \"insert\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"-\"] * (t2b - t2a)\n            news[second] += [\"+\"] * (t2b - t2a)\n\n        elif opc == \"delete\":\n            for name, col in zip(columns, zip(*t1[t1a:t1b])):\n                news[name].extend(col)\n            news[first] += [\"+\"] * (t1b - t1a)\n            news[second] += [\"-\"] * (t1b - t1a)\n\n        elif opc == \"equal\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"=\"] * (t2b - t2a)\n            news[second] += [\"=\"] * (t2b - t2a)\n\n        elif opc == \"replace\":\n            for name, col in zip(columns, zip(*t2[t2a:t2b])):\n                news[name].extend(col)\n            news[first] += [\"r\"] * (t2b - t2a)\n            news[second] += [\"r\"] * (t2b - t2a)\n\n        else:\n            pass\n\n        # Clear cache to free up memory.\n        if len(news[first]) > Config.PAGE_SIZE == 0:\n            for name, L in news.items():\n                new[name].extend(np.array(L))\n                L.clear()\n\n    for name, L in news.items():\n        new[name].extend(np.array(L))\n        L.clear()\n    return new\n
"},{"location":"reference/export_utils/","title":"Export utils","text":""},{"location":"reference/export_utils/#tablite.export_utils","title":"tablite.export_utils","text":""},{"location":"reference/export_utils/#tablite.export_utils-classes","title":"Classes","text":""},{"location":"reference/export_utils/#tablite.export_utils-functions","title":"Functions","text":""},{"location":"reference/export_utils/#tablite.export_utils.to_sql","title":"tablite.export_utils.to_sql(table, name)","text":"

generates ANSI-92 compliant SQL.

PARAMETER DESCRIPTION name

name of SQL table.

TYPE: str

Source code in tablite/export_utils.py
def to_sql(table, name):\n    \"\"\"\n    generates ANSI-92 compliant SQL.\n\n    args:\n        name (str): name of SQL table.\n    \"\"\"\n    sub_cls_check(table, Table)\n    type_check(name, str)\n\n    prefix = name\n    name = \"T1\"\n    create_table = \"\"\"CREATE TABLE {} ({})\"\"\"\n    columns = []\n    for name, col in table.columns.items():\n        dtype = col.types()\n        if len(dtype) == 1:\n            dtype, _ = dtype.popitem()\n            if dtype is int:\n                dtype = \"INTEGER\"\n            elif dtype is float:\n                dtype = \"REAL\"\n            else:\n                dtype = \"TEXT\"\n        else:\n            dtype = \"TEXT\"\n        definition = f\"{name} {dtype}\"\n        columns.append(definition)\n\n    create_table = create_table.format(prefix, \", \".join(columns))\n\n    # return create_table\n    row_inserts = []\n    for row in table.rows:\n        row_inserts.append(str(tuple([i if i is not None else \"NULL\" for i in row])))\n    row_inserts = f\"INSERT INTO {prefix} VALUES \" + \",\".join(row_inserts)\n    return \"begin; {}; {}; commit;\".format(create_table, row_inserts)\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_pandas","title":"tablite.export_utils.to_pandas(table)","text":"

returns pandas.DataFrame

Source code in tablite/export_utils.py
def to_pandas(table):\n    \"\"\"\n    returns pandas.DataFrame\n    \"\"\"\n    sub_cls_check(table, Table)\n    try:\n        return pd.DataFrame(table.to_dict())  # noqa\n    except ImportError:\n        import pandas as pd  # noqa\n    return pd.DataFrame(table.to_dict())  # noqa\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_hdf5","title":"tablite.export_utils.to_hdf5(table, path)","text":"

creates a copy of the table as hdf5

Note that some loss of type information is to be expected in columns of mixed type:

t.show(dtype=True) +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|str |mixed| bool| datetime | date | time | timedelta |str| int |float|int| +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1| |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1|1000|1 | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ t.to_hdf5(filename) t2 = Table.from_hdf5(filename) t2.show(dtype=True) +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|mixed|mixed| bool| datetime | datetime | time | str |str| int |float|int| +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1| 1000| 1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+

Source code in tablite/export_utils.py
def to_hdf5(table, path):\n    # fmt: off\n    \"\"\"\n    creates a copy of the table as hdf5\n\n    Note that some loss of type information is to be expected in columns of mixed type:\n    >>> t.show(dtype=True)\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  | D  |  E  |  F  |         G         |    H     |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|str |mixed| bool|      datetime     |   date   |  time  |   timedelta   |str|           int           |float|int|\n    +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|    |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1|1000|1    | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8  | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    >>> t.to_hdf5(filename)\n    >>> t2 = Table.from_hdf5(filename)\n    >>> t2.show(dtype=True)\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  |  D  |  E  |  F  |         G         |         H         |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|mixed|mixed| bool|      datetime     |      datetime     |  time  |      str      |str|           int           |float|int|\n    +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1| 1000|    1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8  | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    \"\"\"\n    # fmt: in\n    import h5py\n\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    total = f\"{len(table.columns) * len(table):,}\"  # noqa\n    print(f\"writing {total} records to {path}\", end=\"\")\n\n    with h5py.File(path, \"w\") as f:\n        n = 0\n        for name, col in table.items():\n            try:\n                f.create_dataset(name, data=col[:])  # stored in hdf5 as '/name'\n            except TypeError:\n                f.create_dataset(name, data=[str(i) for i in col[:]])  # stored in hdf5 as '/name'\n            n += 1\n    print(\"... done\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.excel_writer","title":"tablite.export_utils.excel_writer(table, path)","text":"

writer for excel files.

This can create xlsx files beyond Excels. If you're using pyexcel to read the data, you'll see the data is there. If you're using Excel, Excel will stop loading after 1,048,576 rows.

See pyexcel for more details: http://docs.pyexcel.org/

Source code in tablite/export_utils.py
def excel_writer(table, path):\n    \"\"\"\n    writer for excel files.\n\n    This can create xlsx files beyond Excels.\n    If you're using pyexcel to read the data, you'll see the data is there.\n    If you're using Excel, Excel will stop loading after 1,048,576 rows.\n\n    See pyexcel for more details:\n    http://docs.pyexcel.org/\n    \"\"\"\n    import pyexcel\n\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    def gen(table):  # local helper\n        yield table.columns\n        for row in table.rows:\n            yield row\n\n    data = list(gen(table))\n    if path.suffix in [\".xls\", \".ods\"]:\n        data = [\n            [str(v) if (isinstance(v, (int, float)) and abs(v) > 2**32 - 1) else DataTypes.to_json(v) for v in row]\n            for row in data\n        ]\n\n    pyexcel.save_as(array=data, dest_file_name=str(path))\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_json","title":"tablite.export_utils.to_json(table, *args, **kwargs)","text":"Source code in tablite/export_utils.py
def to_json(table, *args, **kwargs):\n    import json\n\n    sub_cls_check(table, Table)\n    return json.dumps(table.as_json_serializable())\n
"},{"location":"reference/export_utils/#tablite.export_utils.path_suffix_check","title":"tablite.export_utils.path_suffix_check(path, kind)","text":"Source code in tablite/export_utils.py
def path_suffix_check(path, kind):\n    if not path.suffix == kind:\n        raise ValueError(f\"Suffix mismatch: Expected {kind}, got {path.suffix} in {path.name}\")\n    if not path.parent.exists():\n        raise FileNotFoundError(f\"directory {path.parent} not found.\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.text_writer","title":"tablite.export_utils.text_writer(table, path, tqdm=_tqdm)","text":"

exports table to csv, tsv or txt dependening on path suffix. follows the JSON norm. text escape is ON for all strings.

"},{"location":"reference/export_utils/#tablite.export_utils.text_writer--note","title":"Note:","text":"

If the delimiter is present in a string when the string is exported, text-escape is required, as the format otherwise is corrupted. When the file is being written, it is unknown whether any string in a column contrains the delimiter. As text escaping the few strings that may contain the delimiter would lead to an assymmetric format, the safer guess is to text escape all strings.

Source code in tablite/export_utils.py
def text_writer(table, path, tqdm=_tqdm):\n    \"\"\"exports table to csv, tsv or txt dependening on path suffix.\n    follows the JSON norm. text escape is ON for all strings.\n\n    Note:\n    ----------------------\n    If the delimiter is present in a string when the string is exported,\n    text-escape is required, as the format otherwise is corrupted.\n    When the file is being written, it is unknown whether any string in\n    a column contrains the delimiter. As text escaping the few strings\n    that may contain the delimiter would lead to an assymmetric format,\n    the safer guess is to text escape all strings.\n    \"\"\"\n    sub_cls_check(table, Table)\n    type_check(path, Path)\n\n    def txt(value):  # helper for text writer\n        if value is None:\n            return \"\"  # A column with 1,None,2 must be \"1,,2\".\n        elif isinstance(value, str):\n            # if not (value.startswith('\"') and value.endswith('\"')):\n            #     return f'\"{value}\"'  # this must be escape: \"the quick fox, jumped over the comma\"\n            # else:\n            return value  # this would for example be an empty string: \"\"\n        else:\n            return str(DataTypes.to_json(value))  # this handles datetimes, timedelta, etc.\n\n    delimiters = {\".csv\": \",\", \".tsv\": \"\\t\", \".txt\": \"|\"}\n    delimiter = delimiters.get(path.suffix)\n\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(delimiter.join(c for c in table.columns) + \"\\n\")\n        for row in tqdm(table.rows, total=len(table), disable=Config.TQDM_DISABLE):\n            fo.write(delimiter.join(txt(c) for c in row) + \"\\n\")\n
"},{"location":"reference/export_utils/#tablite.export_utils.sql_writer","title":"tablite.export_utils.sql_writer(table, path)","text":"Source code in tablite/export_utils.py
def sql_writer(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(to_sql(table))\n
"},{"location":"reference/export_utils/#tablite.export_utils.json_writer","title":"tablite.export_utils.json_writer(table, path)","text":"Source code in tablite/export_utils.py
def json_writer(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\") as fo:\n        fo.write(to_json(table))\n
"},{"location":"reference/export_utils/#tablite.export_utils.to_html","title":"tablite.export_utils.to_html(table, path)","text":"Source code in tablite/export_utils.py
def to_html(table, path):\n    type_check(table, Table)\n    type_check(path, Path)\n    with path.open(\"w\", encoding=\"utf-8\") as fo:\n        fo.write(table._repr_html_(slice(0, len(table))))\n
"},{"location":"reference/file_reader_utils/","title":"File reader utils","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils","title":"tablite.file_reader_utils","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-attributes","title":"Attributes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.ENCODING_GUESS_BYTES","title":"tablite.file_reader_utils.ENCODING_GUESS_BYTES = 10000 module-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-classes","title":"Classes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape","title":"tablite.file_reader_utils.TextEscape(openings='({[', closures=']})', text_qualifier='\"', delimiter=',', strip_leading_and_tailing_whitespace=False)","text":"

Bases: object

enables parsing of CSV with respecting brackets and text marks.

Example: text_escape = TextEscape() # set up the instance. for line in somefile.readlines(): list_of_words = text_escape(line) # use the instance. ...

As an example, the Danes and Germans use \" for inches and ' for feet, so we will see data that contains nail (75 x 4 mm, 3\" x 3/12\"), so for this case ( and ) are valid escapes, but \" and ' aren't.

Source code in tablite/file_reader_utils.py
def __init__(\n    self,\n    openings=\"({[\",\n    closures=\"]})\",\n    text_qualifier='\"',\n    delimiter=\",\",\n    strip_leading_and_tailing_whitespace=False,\n):\n    \"\"\"\n    As an example, the Danes and Germans use \" for inches and ' for feet,\n    so we will see data that contains nail (75 x 4 mm, 3\" x 3/12\"), so\n    for this case ( and ) are valid escapes, but \" and ' aren't.\n\n    \"\"\"\n    if openings is None:\n        openings = [None]\n    elif isinstance(openings, str):\n        self.openings = {c for c in openings}\n    else:\n        raise TypeError(f\"expected str, got {type(openings)}\")\n\n    if closures is None:\n        closures = [None]\n    elif isinstance(closures, str):\n        self.closures = {c for c in closures}\n    else:\n        raise TypeError(f\"expected str, got {type(closures)}\")\n\n    if not isinstance(delimiter, str):\n        raise TypeError(f\"expected str, got {type(delimiter)}\")\n    self.delimiter = delimiter\n    self._delimiter_length = len(delimiter)\n    self.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace\n\n    if text_qualifier is None:\n        pass\n    elif text_qualifier in openings + closures:\n        raise ValueError(\"It's a bad idea to have qoute character appears in openings or closures.\")\n    else:\n        self.qoute = text_qualifier\n\n    if not text_qualifier:\n        if not self.strip_leading_and_tailing_whitespace:\n            self.c = self._call_1\n        else:\n            self.c = self._call_2\n    else:\n        self.c = self._call_3\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape-attributes","title":"Attributes","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.openings","title":"tablite.file_reader_utils.TextEscape.openings = {c for c in openings} instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.closures","title":"tablite.file_reader_utils.TextEscape.closures = {c for c in closures} instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.delimiter","title":"tablite.file_reader_utils.TextEscape.delimiter = delimiter instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.strip_leading_and_tailing_whitespace","title":"tablite.file_reader_utils.TextEscape.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.qoute","title":"tablite.file_reader_utils.TextEscape.qoute = text_qualifier instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.c","title":"tablite.file_reader_utils.TextEscape.c = self._call_1 instance-attribute","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape-functions","title":"Functions","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.TextEscape.__call__","title":"tablite.file_reader_utils.TextEscape.__call__(s)","text":"Source code in tablite/file_reader_utils.py
def __call__(self, s):\n    return self.c(s)\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils-functions","title":"Functions","text":""},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.split_by_sequence","title":"tablite.file_reader_utils.split_by_sequence(text, sequence)","text":"

helper to split text according to a split sequence.

Source code in tablite/file_reader_utils.py
def split_by_sequence(text, sequence):\n    \"\"\"helper to split text according to a split sequence.\"\"\"\n    chunks = tuple()\n    for element in sequence:\n        idx = text.find(element)\n        if idx < 0:\n            raise ValueError(f\"'{element}' not in row\")\n        chunk, text = text[:idx], text[len(element) + idx :]\n        chunks += (chunk,)\n    chunks += (text,)  # the remaining text.\n    return chunks\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.detect_seperator","title":"tablite.file_reader_utils.detect_seperator(text)","text":"

:param path: pathlib.Path objects :param encoding: file encoding. :return: 1 character.

Source code in tablite/file_reader_utils.py
def detect_seperator(text):\n    \"\"\"\n    :param path: pathlib.Path objects\n    :param encoding: file encoding.\n    :return: 1 character.\n    \"\"\"\n    # After reviewing the logic in the CSV sniffer, I concluded that all it\n    # really does is to look for a non-text character. As the separator is\n    # determined by the first line, which almost always is a line of headers,\n    # the text characters will be utf-8,16 or ascii letters plus white space.\n    # This leaves the characters ,;:| and \\t as potential separators, with one\n    # exception: files that use whitespace as separator. My logic is therefore\n    # to (1) find the set of characters that intersect with ',;:|\\t' which in\n    # practice is a single character, unless (2) it is empty whereby it must\n    # be whitespace.\n    if len(text) == 0:\n        return None\n    seps = {\",\", \"\\t\", \";\", \":\", \"|\"}.intersection(text)\n    if not seps:\n        if \" \" in text:\n            return \" \"\n        if \"\\n\" in text:\n            return \"\\n\"\n        else:\n            raise ValueError(\"separator not detected\")\n    if len(seps) == 1:\n        return seps.pop()\n    else:\n        frq = [(text.count(i), i) for i in seps]\n        frq.sort(reverse=True)  # most frequent first.\n        return frq[0][-1]\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_headers","title":"tablite.file_reader_utils.get_headers(path, delimiter=None, header_row_index=0, text_qualifier=None, linecount=10)","text":"

file format definition csv comma separated values tsv tab separated values csvz a zip file that contains one or many csv files tsvz a zip file that contains one or many tsv files xls a spreadsheet file format created by MS-Excel 97-2003 xlsx MS-Excel Extensions to the Office Open XML SpreadsheetML File Format. xlsm an MS-Excel Macro-Enabled Workbook file ods open document spreadsheet fods flat open document spreadsheet json java script object notation html html table of the data structure simple simple presentation rst rStructured Text presentation of the data mediawiki media wiki table

Source code in tablite/file_reader_utils.py
def get_headers(path, delimiter=None, header_row_index=0, text_qualifier=None, linecount=10):\n    \"\"\"\n    file format\tdefinition\n    csv\t    comma separated values\n    tsv\t    tab separated values\n    csvz\ta zip file that contains one or many csv files\n    tsvz\ta zip file that contains one or many tsv files\n    xls\t    a spreadsheet file format created by MS-Excel 97-2003\n    xlsx\tMS-Excel Extensions to the Office Open XML SpreadsheetML File Format.\n    xlsm\tan MS-Excel Macro-Enabled Workbook file\n    ods\t    open document spreadsheet\n    fods\tflat open document spreadsheet\n    json\tjava script object notation\n    html\thtml table of the data structure\n    simple\tsimple presentation\n    rst\t    rStructured Text presentation of the data\n    mediawiki\tmedia wiki table\n    \"\"\"\n    if isinstance(path, str):\n        path = Path(path)\n    if not isinstance(path, Path):\n        raise TypeError(\"expected pathlib path.\")\n    if not path.exists():\n        raise FileNotFoundError(str(path))\n    if delimiter is not None:\n        if not isinstance(delimiter, str):\n            raise TypeError(f\"expected str or None, not {type(delimiter)}\")\n\n    delimiters = {\n        \".csv\": \",\",\n        \".tsv\": \"\\t\",\n        \".txt\": None,\n    }\n\n    d = {}\n    if path.suffix not in delimiters:\n        try:\n            book = openpyxl.open(str(path), read_only=True)\n\n            try:\n                all_sheets = book.sheetnames\n\n                for sheet_name, sheet in ((name, book[name]) for name in all_sheets):\n                    fixup_worksheet(sheet)\n                    max_rows = min(sheet.max_row, linecount + 1)\n                    container = [None] * max_rows\n                    padding_ends = 0\n                    max_column = sheet.max_column\n\n                    for i, row_data in enumerate(sheet.iter_rows(0, header_row_index + max_rows, values_only=True), start=-header_row_index):\n                        if i < 0:\n                            # NOTE: for some reason `iter_rows` specifying a start row starts reading cells as binary, instead skip the rows that are before our first read row\n                            continue\n\n                        # NOTE: text readers do not cast types and give back strings, neither should xlsx reader, can't find documentation if it's possible to ignore this via `iter_rows` instead of casting back to string\n                        container[i] = [DataTypes.to_json(v) for v in row_data]\n\n                        for j, cell in enumerate(reversed(row_data)):\n                            if cell is None:\n                                continue\n\n                            padding_ends = max(padding_ends, max_column - j)\n\n                            break\n\n                    d[sheet_name] = [c[0:padding_ends] for c in container]\n                    d[\"delimiter\"] = None\n            finally:\n                book.close()\n\n            return d\n        except Exception:\n            pass  # it must be a raw text format.\n\n    try:\n        with path.open(\"rb\") as fi:\n            rawdata = fi.read(ENCODING_GUESS_BYTES)\n            encoding = chardet.detect(rawdata)[\"encoding\"]\n        with path.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:\n            lines = []\n            for n, line in enumerate(fi, -header_row_index):\n                if n < 0:\n                    continue\n                line = line.rstrip(\"\\n\")\n                lines.append(line)\n                if n >= linecount:\n                    break  # break on first\n\n            if delimiter is None:\n                try:\n                    d[\"delimiter\"] = delimiter = detect_seperator(\"\\n\".join(lines))\n                except ValueError as e:\n                    if e.args == (\"separator not detected\", ):\n                        d[\"delimiter\"] = delimiter = None # this will handle the case of 1 column, 1 row\n                    else:\n                        raise e\n\n            if delimiter is None:\n                d[\"delimiter\"] = delimiter = delimiters[path.suffix]  # pickup the default one\n                d[\"is_empty\"] = True  # mark as empty to return an empty table instead of throwing\n\n            text_escape = TextEscape(text_qualifier=text_qualifier, delimiter=delimiter)\n\n            d[path.name] = [text_escape(line) for line in lines]\n        return d\n    except Exception:\n        raise ValueError(f\"can't read {path.suffix}\")\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_encoding","title":"tablite.file_reader_utils.get_encoding(path, nbytes=ENCODING_GUESS_BYTES)","text":"Source code in tablite/file_reader_utils.py
def get_encoding(path, nbytes=ENCODING_GUESS_BYTES):\n    nbytes = min(nbytes, path.stat().st_size)\n    with path.open(\"rb\") as fi:\n        rawdata = fi.read(nbytes)\n        encoding = chardet.detect(rawdata)[\"encoding\"]\n        if encoding == \"ascii\":  # utf-8 is backwards compatible with ascii\n            return \"utf-8\"  # --   so should the first 10k chars not be enough,\n        return encoding  # --      the utf-8 encoding will still get it right.\n
"},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.get_delimiter","title":"tablite.file_reader_utils.get_delimiter(path, encoding)","text":"Source code in tablite/file_reader_utils.py
def get_delimiter(path, encoding):\n    with path.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:\n        lines = []\n        for n, line in enumerate(fi):\n            line = line.rstrip(\"\\n\")\n            lines.append(line)\n            if n > 10:\n                break  # break on first\n        delimiter = detect_seperator(\"\\n\".join(lines))\n        if delimiter is None:\n            raise ValueError(\"Delimiter could not be determined\")\n        return delimiter\n
"},{"location":"reference/groupby_utils/","title":"Groupby utils","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils","title":"tablite.groupby_utils","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils-classes","title":"Classes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupbyFunction","title":"tablite.groupby_utils.GroupbyFunction","text":"

Bases: object

"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit","title":"tablite.groupby_utils.Limit()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = None\n    self.f = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.value","title":"tablite.groupby_utils.Limit.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.f","title":"tablite.groupby_utils.Limit.f = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Limit.update","title":"tablite.groupby_utils.Limit.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max","title":"tablite.groupby_utils.Max()","text":"

Bases: Limit

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n    self.f = max\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.value","title":"tablite.groupby_utils.Max.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.f","title":"tablite.groupby_utils.Max.f = max instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Max.update","title":"tablite.groupby_utils.Max.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min","title":"tablite.groupby_utils.Min()","text":"

Bases: Limit

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n    self.f = min\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.value","title":"tablite.groupby_utils.Min.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.f","title":"tablite.groupby_utils.Min.f = min instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Min.update","title":"tablite.groupby_utils.Min.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if value is None:\n        pass\n    elif self.value is None:\n        self.value = value\n    else:\n        self.value = self.f((value, self.value))\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum","title":"tablite.groupby_utils.Sum()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum.value","title":"tablite.groupby_utils.Sum.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Sum.update","title":"tablite.groupby_utils.Sum.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (type(None), date, time, datetime, str)):\n        raise ValueError(f\"Sum of {type(value)} doesn't make sense.\")\n    self.value += value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product","title":"tablite.groupby_utils.Product()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self) -> None:\n    self.value = 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product.value","title":"tablite.groupby_utils.Product.value = 1 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Product.update","title":"tablite.groupby_utils.Product.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value *= value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.First","title":"tablite.groupby_utils.First()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = self.empty\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.First-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.empty","title":"tablite.groupby_utils.First.empty = (None) class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.value","title":"tablite.groupby_utils.First.value = self.empty instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.First.update","title":"tablite.groupby_utils.First.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if self.value is First.empty:\n        self.value = value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last","title":"tablite.groupby_utils.Last()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last.value","title":"tablite.groupby_utils.Last.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Last.update","title":"tablite.groupby_utils.Last.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value = value\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count","title":"tablite.groupby_utils.Count()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count.value","title":"tablite.groupby_utils.Count.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Count.update","title":"tablite.groupby_utils.Count.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.value += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique","title":"tablite.groupby_utils.CountUnique()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.items = set()\n    self.value = None\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.items","title":"tablite.groupby_utils.CountUnique.items = set() instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.value","title":"tablite.groupby_utils.CountUnique.value = None instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.CountUnique.update","title":"tablite.groupby_utils.CountUnique.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.items.add(value)\n    self.value = len(self.items)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average","title":"tablite.groupby_utils.Average()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.sum = 0\n    self.count = 0\n    self.value = 0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.sum","title":"tablite.groupby_utils.Average.sum = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.count","title":"tablite.groupby_utils.Average.count = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.value","title":"tablite.groupby_utils.Average.value = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Average.update","title":"tablite.groupby_utils.Average.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (date, time, datetime, str)):\n        raise ValueError(f\"Sum of {type(value)} doesn't make sense.\")\n    if value is not None:\n        self.sum += value\n        self.count += 1\n        self.value = self.sum / self.count\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation","title":"tablite.groupby_utils.StandardDeviation()","text":"

Bases: GroupbyFunction

Uses J.P. Welfords (1962) algorithm. For details see https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.count = 0\n    self.mean = 0\n    self.c = 0.0\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.count","title":"tablite.groupby_utils.StandardDeviation.count = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.mean","title":"tablite.groupby_utils.StandardDeviation.mean = 0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.c","title":"tablite.groupby_utils.StandardDeviation.c = 0.0 instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.value","title":"tablite.groupby_utils.StandardDeviation.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.StandardDeviation.update","title":"tablite.groupby_utils.StandardDeviation.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    if isinstance(value, (date, time, datetime, str)):\n        raise ValueError(f\"Std.dev. of {type(value)} doesn't make sense.\")\n    if value is not None:\n        self.count += 1\n        dt = value - self.mean\n        self.mean += dt / self.count\n        self.c += dt * (value - self.mean)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram","title":"tablite.groupby_utils.Histogram()","text":"

Bases: GroupbyFunction

Source code in tablite/groupby_utils.py
def __init__(self):\n    self.hist = defaultdict(int)\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram.hist","title":"tablite.groupby_utils.Histogram.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Histogram.update","title":"tablite.groupby_utils.Histogram.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median","title":"tablite.groupby_utils.Median()","text":"

Bases: Histogram

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.hist","title":"tablite.groupby_utils.Median.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.value","title":"tablite.groupby_utils.Median.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Median.update","title":"tablite.groupby_utils.Median.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode","title":"tablite.groupby_utils.Mode()","text":"

Bases: Histogram

Source code in tablite/groupby_utils.py
def __init__(self):\n    super().__init__()\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.hist","title":"tablite.groupby_utils.Mode.hist = defaultdict(int) instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.value","title":"tablite.groupby_utils.Mode.value property","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode-functions","title":"Functions","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.Mode.update","title":"tablite.groupby_utils.Mode.update(value)","text":"Source code in tablite/groupby_utils.py
def update(self, value):\n    self.hist[value] += 1\n
"},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy","title":"tablite.groupby_utils.GroupBy","text":"

Bases: object

"},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy-attributes","title":"Attributes","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.max","title":"tablite.groupby_utils.GroupBy.max = Max class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.min","title":"tablite.groupby_utils.GroupBy.min = Min class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.sum","title":"tablite.groupby_utils.GroupBy.sum = Sum class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.product","title":"tablite.groupby_utils.GroupBy.product = Product class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.first","title":"tablite.groupby_utils.GroupBy.first = First class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.last","title":"tablite.groupby_utils.GroupBy.last = Last class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.count","title":"tablite.groupby_utils.GroupBy.count = Count class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.count_unique","title":"tablite.groupby_utils.GroupBy.count_unique = CountUnique class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.avg","title":"tablite.groupby_utils.GroupBy.avg = Average class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.stdev","title":"tablite.groupby_utils.GroupBy.stdev = StandardDeviation class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.median","title":"tablite.groupby_utils.GroupBy.median = Median class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.mode","title":"tablite.groupby_utils.GroupBy.mode = Mode class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.functions","title":"tablite.groupby_utils.GroupBy.functions = [Max, Min, Sum, First, Last, Product, Count, CountUnique, Average, StandardDeviation, Median, Mode] class-attribute instance-attribute","text":""},{"location":"reference/groupby_utils/#tablite.groupby_utils.GroupBy.function_names","title":"tablite.groupby_utils.GroupBy.function_names = {f.__name__: ffor f in functions} class-attribute instance-attribute","text":""},{"location":"reference/groupbys/","title":"Groupbys","text":""},{"location":"reference/groupbys/#tablite.groupbys","title":"tablite.groupbys","text":""},{"location":"reference/groupbys/#tablite.groupbys-classes","title":"Classes","text":""},{"location":"reference/groupbys/#tablite.groupbys-functions","title":"Functions","text":""},{"location":"reference/groupbys/#tablite.groupbys.groupby","title":"tablite.groupbys.groupby(T, keys, functions, tqdm=_tqdm, pbar=None)","text":"

keys: column names for grouping. functions: [optional] list of column names and group functions (See GroupyBy class) returns: table

Example:

>>> t = Table()\n>>> t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n>>> t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n>>> t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n>>> t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n>>> g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n>>> g.show()\n+===+===+===+======+\n| # | A | C |Sum(B)|\n|row|int|int| int  |\n+---+---+---+------+\n|0  |  1|  6|     2|\n|1  |  1|  5|     4|\n|2  |  2|  4|     6|\n|3  |  2|  3|     8|\n|4  |  3|  2|    10|\n|5  |  3|  1|    12|\n+===+===+===+======+\n

Cheat sheet:

list of unique values

>>> g1 = t.groupby(keys=['A'], functions=[])\n>>> g1['A'][:]\n[1,2,3]\n

alternatively:

>>> t['A'].unique()\n[1,2,3]\n

list of unique values, grouped by longest combination.

>>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n>>> g2['A'][:], g2['B'][:]\n([1,1,2,2,3,3], [1,2,3,4,5,6])\n

alternatively use:

>>> list(zip(*t.index('A', 'B').keys()))\n[(1,1,2,2,3,3) (1,2,3,4,5,6)]\n

A key (unique values) and count hereof.

>>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n>>> g3['A'][:], g3['Count(A)'][:]\n([1,2,3], [4,4,4])\n

alternatively use:

>>> t['A'].histogram()\n([1,2,3], [4,4,4])\n

for more examples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py

Source code in tablite/groupbys.py
def groupby(\n    T, keys, functions, tqdm=_tqdm, pbar=None\n):  # TODO: This is single core code.\n    \"\"\"\n    keys: column names for grouping.\n    functions: [optional] list of column names and group functions (See GroupyBy class)\n    returns: table\n\n    Example:\n    ```\n    >>> t = Table()\n    >>> t.add_column('A', data=[1, 1, 2, 2, 3, 3] * 2)\n    >>> t.add_column('B', data=[1, 2, 3, 4, 5, 6] * 2)\n    >>> t.add_column('C', data=[6, 5, 4, 3, 2, 1] * 2)\n    >>> t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n    >>> g = t.groupby(keys=['A', 'C'], functions=[('B', gb.sum)])\n    >>> g.show()\n    +===+===+===+======+\n    | # | A | C |Sum(B)|\n    |row|int|int| int  |\n    +---+---+---+------+\n    |0  |  1|  6|     2|\n    |1  |  1|  5|     4|\n    |2  |  2|  4|     6|\n    |3  |  2|  3|     8|\n    |4  |  3|  2|    10|\n    |5  |  3|  1|    12|\n    +===+===+===+======+\n    ```\n\n    Cheat sheet:\n\n    list of unique values\n    ```\n    >>> g1 = t.groupby(keys=['A'], functions=[])\n    >>> g1['A'][:]\n    [1,2,3]\n    ```\n    alternatively:\n    ```\n    >>> t['A'].unique()\n    [1,2,3]\n    ```\n    list of unique values, grouped by longest combination.\n    ```\n    >>> g2 = t.groupby(keys=['A', 'B'], functions=[])\n    >>> g2['A'][:], g2['B'][:]\n    ([1,1,2,2,3,3], [1,2,3,4,5,6])\n    ```\n    alternatively use:\n    ```\n    >>> list(zip(*t.index('A', 'B').keys()))\n    [(1,1,2,2,3,3) (1,2,3,4,5,6)]\n    ```\n\n    A key (unique values) and count hereof.\n    ```\n    >>> g3 = t.groupby(keys=['A'], functions=[('A', gb.count)])\n    >>> g3['A'][:], g3['Count(A)'][:]\n    ([1,2,3], [4,4,4])\n    ```\n    alternatively use:\n    ```\n    >>> t['A'].histogram()\n    ([1,2,3], [4,4,4])\n    ```\n    for more examples see: https://github.com/root-11/tablite/blob/master/tests/test_groupby.py\n\n    \"\"\"\n    if not isinstance(keys, list):\n        raise TypeError(\"expected keys as a list of column names\")\n\n    if keys:\n        if len(set(keys)) != len(keys):\n            duplicates = [k for k in keys if keys.count(k) > 1]\n            s = \"\" if len(duplicates) > 1 else \"s\"\n            raise ValueError(\n                f\"duplicate key{s} found across rows and columns: {duplicates}\"\n            )\n\n    if not isinstance(functions, list):\n        raise TypeError(\n            f\"Expected functions to be a list of tuples. Got {type(functions)}\"\n        )\n\n    if not keys + functions:\n        raise ValueError(\"No keys or functions?\")\n\n    if not all(len(i) == 2 for i in functions):\n        raise ValueError(\n            f\"Expected each tuple in functions to be of length 2. \\nGot {functions}\"\n        )\n\n    if not all(isinstance(a, str) for a, _ in functions):\n        L = [(a, type(a)) for a, _ in functions if not isinstance(a, str)]\n        raise ValueError(\n            f\"Expected column names in functions to be strings. Found: {L}\"\n        )\n\n    if not all(\n        issubclass(b, GroupbyFunction) and b in GroupBy.functions for _, b in functions\n    ):\n        L = [b for _, b in functions if b not in GroupBy._functions]\n        if len(L) == 1:\n            singular = f\"function {L[0]} is not in GroupBy.functions\"\n            raise ValueError(singular)\n        else:\n            plural = f\"the functions {L} are not in GroupBy.functions\"\n            raise ValueError(plural)\n\n    # only keys will produce unique values for each key group.\n    if keys and not functions:\n        cols = list(zip(*T.index(*keys)))\n        result = T.__class__()\n\n        pbar = tqdm(total=len(keys), desc=\"groupby\") if pbar is None else pbar\n\n        for col_name, col in zip(keys, cols):\n            result[col_name] = col\n\n            pbar.update(1)\n        return result\n\n    # grouping is required...\n    # 1. Aggregate data.\n    aggregation_functions = defaultdict(dict)\n    cols = keys + [col_name for col_name, _ in functions]\n    seen, L = set(), []\n    for c in cols:  # maintains order of appearance.\n        if c not in seen:\n            seen.add(c)\n            L.append(c)\n\n    # there's a table of values.\n    data = T[L]\n    if isinstance(data, Column):\n        tbl = Table()\n        tbl[L[0]] = data\n    else:\n        tbl = data\n\n    pbar = (\n        tqdm(desc=\"groupby\", total=len(tbl), disable=Config.TQDM_DISABLE)\n        if pbar is None\n        else pbar\n    )\n\n    for row in tbl.rows:\n        d = {col_name: value for col_name, value in zip(L, row)}\n        key = tuple([d[k] for k in keys])\n        agg_functions = aggregation_functions.get(key)\n        if not agg_functions:\n            aggregation_functions[key] = agg_functions = [\n                (col_name, f()) for col_name, f in functions\n            ]\n        for col_name, f in agg_functions:\n            f.update(d[col_name])\n\n        pbar.update(1)\n\n    # 2. make dense table.\n    cols = [[] for _ in cols]\n    for key_tuple, funcs in aggregation_functions.items():\n        for ix, key_value in enumerate(key_tuple):\n            cols[ix].append(key_value)\n        for ix, (_, f) in enumerate(funcs, start=len(keys)):\n            cols[ix].append(f.value)\n\n    new_names = keys + [f\"{f.__name__}({col_name})\" for col_name, f in functions]\n    result = type(T)()  # New Table.\n    for ix, (col_name, data) in enumerate(zip(new_names, cols)):\n        revised_name = unique_name(col_name, result.columns)\n        result[revised_name] = data\n    return result\n
"},{"location":"reference/import_utils/","title":"Import utils","text":""},{"location":"reference/import_utils/#tablite.import_utils","title":"tablite.import_utils","text":""},{"location":"reference/import_utils/#tablite.import_utils-attributes","title":"Attributes","text":""},{"location":"reference/import_utils/#tablite.import_utils.file_readers","title":"tablite.import_utils.file_readers = {'fods': excel_reader, 'json': excel_reader, 'html': from_html, 'hdf5': from_hdf5, 'simple': excel_reader, 'rst': excel_reader, 'mediawiki': excel_reader, 'xlsx': excel_reader, 'xls': excel_reader, 'xlsm': excel_reader, 'csv': text_reader, 'tsv': text_reader, 'txt': text_reader, 'ods': ods_reader} module-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.valid_readers","title":"tablite.import_utils.valid_readers = ','.join(list(file_readers.keys())) module-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils-classes","title":"Classes","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig","title":"tablite.import_utils.TRconfig(source, destination, start, end, guess_datatypes, delimiter, text_qualifier, text_escape_openings, text_escape_closures, strip_leading_and_tailing_whitespace, encoding, newline_offsets, fields)","text":"

Bases: object

Source code in tablite/import_utils.py
def __init__(\n    self,\n    source,\n    destination,\n    start,\n    end,\n    guess_datatypes,\n    delimiter,\n    text_qualifier,\n    text_escape_openings,\n    text_escape_closures,\n    strip_leading_and_tailing_whitespace,\n    encoding,\n    newline_offsets,\n    fields\n) -> None:\n    self.source = source\n    self.destination = destination\n    self.start = start\n    self.end = end\n    self.guess_datatypes = guess_datatypes\n    self.delimiter = delimiter\n    self.text_qualifier = text_qualifier\n    self.text_escape_openings = text_escape_openings\n    self.text_escape_closures = text_escape_closures\n    self.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace\n    self.encoding = encoding\n    self.newline_offsets = newline_offsets\n    self.fields = fields\n    type_check(start, int),\n    type_check(end, int),\n    type_check(delimiter, str),\n    type_check(text_qualifier, (str, type(None))),\n    type_check(text_escape_openings, str),\n    type_check(text_escape_closures, str),\n    type_check(encoding, str),\n    type_check(strip_leading_and_tailing_whitespace, bool),\n    type_check(newline_offsets, list)\n    type_check(fields, dict)\n
"},{"location":"reference/import_utils/#tablite.import_utils.TRconfig-attributes","title":"Attributes","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.source","title":"tablite.import_utils.TRconfig.source = source instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.destination","title":"tablite.import_utils.TRconfig.destination = destination instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.start","title":"tablite.import_utils.TRconfig.start = start instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.end","title":"tablite.import_utils.TRconfig.end = end instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.guess_datatypes","title":"tablite.import_utils.TRconfig.guess_datatypes = guess_datatypes instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.delimiter","title":"tablite.import_utils.TRconfig.delimiter = delimiter instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_qualifier","title":"tablite.import_utils.TRconfig.text_qualifier = text_qualifier instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_escape_openings","title":"tablite.import_utils.TRconfig.text_escape_openings = text_escape_openings instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.text_escape_closures","title":"tablite.import_utils.TRconfig.text_escape_closures = text_escape_closures instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.strip_leading_and_tailing_whitespace","title":"tablite.import_utils.TRconfig.strip_leading_and_tailing_whitespace = strip_leading_and_tailing_whitespace instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.encoding","title":"tablite.import_utils.TRconfig.encoding = encoding instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.newline_offsets","title":"tablite.import_utils.TRconfig.newline_offsets = newline_offsets instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.fields","title":"tablite.import_utils.TRconfig.fields = fields instance-attribute","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig-functions","title":"Functions","text":""},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.copy","title":"tablite.import_utils.TRconfig.copy()","text":"Source code in tablite/import_utils.py
def copy(self):\n    return TRconfig(**self.dict())\n
"},{"location":"reference/import_utils/#tablite.import_utils.TRconfig.dict","title":"tablite.import_utils.TRconfig.dict()","text":"Source code in tablite/import_utils.py
def dict(self):\n    return {k: v for k, v in self.__dict__.items() if not (k.startswith(\"_\") or callable(v))}\n
"},{"location":"reference/import_utils/#tablite.import_utils-functions","title":"Functions","text":""},{"location":"reference/import_utils/#tablite.import_utils.from_pandas","title":"tablite.import_utils.from_pandas(T, df)","text":"

Creates Table using pd.to_dict('list')

similar to:

import pandas as pd df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]}) df a b 0 1 4 1 2 5 2 3 6 df.to_dict('list')

t = Table.from_dict(df.to_dict('list)) t.show() +===+===+===+ | # | a | b | |row|int|int| +---+---+---+ | 0 | 1| 4| | 1 | 2| 5| | 2 | 3| 6| +===+===+===+

Source code in tablite/import_utils.py
def from_pandas(T, df):\n    \"\"\"\n    Creates Table using pd.to_dict('list')\n\n    similar to:\n    >>> import pandas as pd\n    >>> df = pd.DataFrame({'a':[1,2,3], 'b':[4,5,6]})\n    >>> df\n        a  b\n        0  1  4\n        1  2  5\n        2  3  6\n    >>> df.to_dict('list')\n    {'a': [1, 2, 3], 'b': [4, 5, 6]}\n\n    >>> t = Table.from_dict(df.to_dict('list))\n    >>> t.show()\n        +===+===+===+\n        | # | a | b |\n        |row|int|int|\n        +---+---+---+\n        | 0 |  1|  4|\n        | 1 |  2|  5|\n        | 2 |  3|  6|\n        +===+===+===+\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    return T(columns=df.to_dict(\"list\"))  # noqa\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_hdf5","title":"tablite.import_utils.from_hdf5(T, path, tqdm=_tqdm, pbar=None)","text":"

imports an exported hdf5 table.

Note that some loss of type information is to be expected in columns of mixed type:

t.show(dtype=True) +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|str |mixed| bool| datetime | date | time | timedelta |str| int |float|int| +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1| |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1|1000|1 | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+ t.to_hdf5(filename) t2 = Table.from_hdf5(filename) t2.show(dtype=True) +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+ | # | A | B | C | D | E | F | G | H | I | J | K | L | M | O | |row|int|mixed|float|mixed|mixed| bool| datetime | datetime | time | str |str| int |float|int| +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+ | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b |-100000000000000000000000| inf| 11| | 1 | 1| 1| 1.1| 1000| 1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11| +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+

Source code in tablite/import_utils.py
def from_hdf5(T, path, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    imports an exported hdf5 table.\n\n    Note that some loss of type information is to be expected in columns of mixed type:\n    >>> t.show(dtype=True)\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  | D  |  E  |  F  |         G         |    H     |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|str |mixed| bool|      datetime     |   date   |  time  |   timedelta   |str|           int           |float|int|\n    +---+---+-----+-----+----+-----+-----+-------------------+----------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|    |None |False|2023-06-09 09:12:06|2023-06-09|09:12:06| 1 day, 0:00:00|b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1|1000|1    | True|2023-06-09 09:12:06|2023-06-09|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+====+=====+=====+===================+==========+========+===============+===+=========================+=====+===+\n    >>> t.to_hdf5(filename)\n    >>> t2 = Table.from_hdf5(filename)\n    >>> t2.show(dtype=True)\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    | # | A |  B  |  C  |  D  |  E  |  F  |         G         |         H         |   I    |       J       | K |            L            |  M  | O |\n    |row|int|mixed|float|mixed|mixed| bool|      datetime     |      datetime     |  time  |      str      |str|           int           |float|int|\n    +---+---+-----+-----+-----+-----+-----+-------------------+-------------------+--------+---------------+---+-------------------------+-----+---+\n    | 0 | -1|None | -1.1|None |None |False|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|1 day, 0:00:00 |b  |-100000000000000000000000|  inf| 11|\n    | 1 |  1|    1|  1.1| 1000|    1| True|2023-06-09 09:12:06|2023-06-09 00:00:00|09:12:06|2 days, 0:06:40|\u55e8 | 100000000000000000000000| -inf|-11|\n    +===+===+=====+=====+=====+=====+=====+===================+===================+========+===============+===+=========================+=====+===+\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    import h5py\n\n    type_check(path, Path)\n    t = T()\n    with h5py.File(path, \"r\") as h5:\n        for col_name in h5.keys():\n            dset = h5[col_name]\n            arr = np.array(dset[:])\n            if arr.dtype == object:\n                arr = np.array(DataTypes.guess([v.decode(\"utf-8\") for v in arr]))\n            t[col_name] = arr\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_json","title":"tablite.import_utils.from_json(T, jsn)","text":"

Imports tables exported using .to_json

Source code in tablite/import_utils.py
def from_json(T, jsn):\n    \"\"\"\n    Imports tables exported using .to_json\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    import json\n\n    type_check(jsn, str)\n    d = json.loads(jsn)\n    return T(columns=d[\"columns\"])\n
"},{"location":"reference/import_utils/#tablite.import_utils.from_html","title":"tablite.import_utils.from_html(T, path, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/import_utils.py
def from_html(T, path, tqdm=_tqdm, pbar=None):\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n    type_check(path, Path)\n\n    if pbar is None:\n        total = path.stat().st_size\n        pbar = tqdm(total=total, desc=\"from_html\", disable=Config.TQDM_DISABLE)\n\n    row_start, row_end = \"<tr>\", \"</tr>\"\n    value_start, value_end = \"<th>\", \"</th>\"\n    chunk = \"\"\n    t = None  # will be T()\n    start, end = 0, 0\n    data = {}\n    with path.open(\"r\") as fi:\n        while True:\n            start = chunk.find(row_start, start)  # row tag start\n            end = chunk.find(row_end, end)  # row tag end\n            if start == -1 or end == -1:\n                new = fi.read(100_000)\n                pbar.update(len(new))\n                if new == \"\":\n                    break\n                chunk += new\n                continue\n            # get indices from chunk\n            row = chunk[start + len(row_start) : end]\n            fields = [v.rstrip(value_end) for v in row.split(value_start)]\n            if not data:\n                headers = fields[:]\n                data = {f: [] for f in headers}\n                continue\n            else:\n                for field, header in zip(fields, headers):\n                    data[header].append(field)\n\n            chunk = chunk[end + len(row_end) :]\n\n            if len(data[headers[0]]) == Config.PAGE_SIZE:\n                if t is None:\n                    t = T(columns=data)\n                else:\n                    for k, v in data.items():\n                        t[k].extend(DataTypes.guess(v))\n                data = {f: [] for f in headers}\n\n    for k, v in data.items():\n        t[k].extend(DataTypes.guess(v))\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.excel_reader","title":"tablite.import_utils.excel_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, tqdm=_tqdm, **kwargs)","text":"

returns Table from excel

**kwargs are excess arguments that are ignored.

Source code in tablite/import_utils.py
def excel_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, tqdm=_tqdm, **kwargs):\n    \"\"\"\n    returns Table from excel\n\n    **kwargs are excess arguments that are ignored.\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    book = openpyxl.load_workbook(path, read_only=True, data_only=True)\n\n    if sheet is None:  # help the user.\n        sheet_list = ', '.join((f'\\n - {c}' for c in book.sheetnames))\n        raise ValueError(f\"No 'sheet' declared, available sheets:{sheet_list}\")\n    elif sheet not in book.sheetnames:\n        raise ValueError(f\"sheet not found: {sheet}\")\n\n    if not (isinstance(start, int) and start >= 0):\n        raise ValueError(\"expected start as an integer >=0\")\n    if not (isinstance(limit, int) and limit > 0):\n        raise ValueError(\"expected limit as integer > 0\")\n\n    worksheet = book[sheet]\n    fixup_worksheet(worksheet)\n\n    try:\n        # get the first row to know our headers or the number of columns\n        fields = [str(c.value) for c in next(worksheet.iter_rows(min_row=header_row_index + 1))] # excel is offset by 1\n    except StopIteration:\n        # excel was empty, return empty table\n        return T()\n\n    if not first_row_has_headers:\n        # since the first row did not contain headers, we use the column count to populate header names\n        fields = [str(i) for i in range(len(fields))]\n\n    if columns is None:\n        # no columns were specified by user to import, that means we import all of the them\n        columns = []\n\n        for f in fields:\n            # fixup the duplicate column names\n            columns.append(unique_name(f, columns))\n\n        field_dict = {k: i for i, k in enumerate(columns)}\n    else:\n        field_dict = {}\n\n        for k, i in ((k, fields.index(k)) for k in columns):\n            # fixup the duplicate column names\n            field_dict[unique_name(k, field_dict.keys())] = i\n\n    # calculate our data rows iterator offset\n    it_offset = start + (1 if first_row_has_headers else 0) + header_row_index + 1\n\n    # attempt to fetch number of rows in the sheet\n    total_rows = worksheet.max_row\n    real_tqdm = True\n\n    if total_rows is None:\n        # i don't know what causes it but max_row can be None in some cases, so we don't know how large the dataset is\n        total_rows = it_offset + limit\n        real_tqdm = False\n\n    # create the actual data rows iterator\n    it_rows = worksheet.iter_rows(min_row=it_offset, max_row=min(it_offset+limit, total_rows))\n    it_used_indices = list(field_dict.values())\n\n    # filter columns that we're not going to use\n    it_rows_filtered = ([row[idx].value for idx in it_used_indices] for row in it_rows)\n\n    # create page directory\n    workdir = Path(Config.workdir) / Config.pid\n    pagesdir = workdir/\"pages\"\n    pagesdir.mkdir(exist_ok=True, parents=True)\n\n    field_names = list(field_dict.keys())\n    column_count = len(field_names)\n\n    page_fhs = None\n\n    # prepopulate the table with columns\n    table = T()\n    for name in field_names:\n        table[name] = Column(table.path)\n\n    pbar_fname = path.name\n    if len(pbar_fname) > 20:\n        pbar_fname = pbar_fname[0:10] + \"...\" + pbar_fname[-7:]\n\n    if real_tqdm:\n        # we can create a true tqdm progress bar, make one\n        tqdm_iter = tqdm(it_rows_filtered, total=total_rows, desc=f\"importing excel: {pbar_fname}\")\n    else:\n        \"\"\"\n            openpyxls was unable to precalculate the size of the excel for whatever reason\n            forcing recalc would require parsing entire file\n            drop the progress bar in that case, just show iterations\n\n            as an alternative we can use \u03a3=1/x but it just doesn't look good, show iterations per second instead\n        \"\"\"\n        tqdm_iter = tqdm(it_rows_filtered, desc=f\"importing excel: {pbar_fname}\")\n\n    tqdm_iter = enumerate(tqdm_iter)\n\n    while True:\n        try:\n            idx, row = next(tqdm_iter)\n        except StopIteration:\n            break # because in some cases we can't know the size of excel to set the upper iterator limit we loop until stop iteration is encountered\n\n        if idx % Config.PAGE_SIZE == 0:\n            if page_fhs is not None:\n                # we reached the max page file size, fix the pages\n                [_fix_xls_page(table, c, fh) for c, fh in zip(field_names, page_fhs)]\n\n            page_fhs = [None] * column_count\n\n            for cidx in range(column_count):\n                # allocate new pages\n                pg_path = pagesdir / f\"{next(Page.ids)}.npy\"\n                page_fhs[cidx] = open(pg_path, \"wb\")\n\n        for fh, value in zip(page_fhs, row):\n            \"\"\"\n                since excel types are already cast into appropriate type we're going to do two passes per page\n\n                we create our temporary custom format:\n                packed type|packed byte count|packed bytes|...\n\n                available types:\n                    * q - int64\n                    * d - float64\n                    * s - string\n                    * b - boolean\n                    * n - none\n                    * p - pickled (date, time, datetime)\n            \"\"\"\n            dtype = type(value)\n\n            if dtype == int:\n                ptype, bytes_ = b'q', struct.pack('q', value) # pack int as int64\n            elif dtype == float:\n                ptype, bytes_ = b'd', struct.pack('d', value) # pack float as float64\n            elif dtype == str:\n                ptype, bytes_ = b's', value.encode(\"utf-8\")   # pack string\n            elif dtype == bool:\n                ptype, bytes_ = b'b', b'1' if value else b'0' # pack boolean\n            elif value is None:\n                ptype, bytes_ = b'n', b''                     # pack none\n            elif dtype in [date, time, datetime]:\n                ptype, bytes_ = b'p', pkl.dumps(value)        # pack object types via pickle\n            else:\n                raise NotImplementedError()\n\n            byte_count = struct.pack('I', len(bytes_))        # pack our payload size, i doubt payload size can be over uint32\n\n            # dump object to file\n            fh.write(ptype)\n            fh.write(byte_count)\n            fh.write(bytes_)\n\n    if page_fhs is not None:\n        # we reached end of the loop, fix the pages\n        [_fix_xls_page(table, c, fh) for c, fh in zip(field_names, page_fhs)]\n\n    return table\n
"},{"location":"reference/import_utils/#tablite.import_utils.ods_reader","title":"tablite.import_utils.ods_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, **kwargs)","text":"

returns Table from .ODS

Source code in tablite/import_utils.py
def ods_reader(T, path, first_row_has_headers=True, header_row_index=0, sheet=None, columns=None, start=0, limit=sys.maxsize, **kwargs):\n    \"\"\"\n    returns Table from .ODS\n    \"\"\"\n    if not issubclass(T, Table):\n        raise TypeError(\"Expected subclass of Table\")\n\n    sheets = pyexcel.get_book_dict(file_name=str(path))\n\n    if sheet is None or sheet not in sheets:\n        raise ValueError(f\"No sheet_name declared: \\navailable sheets:\\n{[s.name for s in sheets]}\")\n\n    data = sheets[sheet]\n    for _ in range(len(data)):  # remove empty lines at the end of the data.\n        if \"\" == \"\".join(str(i) for i in data[-1]):\n            data = data[:-1]\n        else:\n            break\n\n    if not (isinstance(start, int) and start >= 0):\n        raise ValueError(\"expected start as an integer >=0\")\n    if not (isinstance(limit, int) and limit > 0):\n        raise ValueError(\"expected limit as integer > 0\")\n\n    t = T()\n\n    used_columns_names = set()\n    for ix, value in enumerate(data[header_row_index]):\n        if first_row_has_headers:\n            header, start_row_pos = str(value), (1 + header_row_index)\n        else:\n            header, start_row_pos = f\"_{ix + 1}\", (0 + header_row_index)\n\n        if columns is not None:\n            if header not in columns:\n                continue\n\n        unique_column_name = unique_name(str(header), used_columns_names)\n        used_columns_names.add(unique_column_name)\n\n        t[unique_column_name] = [row[ix] for row in data[start_row_pos : start_row_pos + limit] if len(row) > ix]\n    return t\n
"},{"location":"reference/import_utils/#tablite.import_utils.text_reader_task","title":"tablite.import_utils.text_reader_task(source, destination, start, end, guess_datatypes, delimiter, text_qualifier, text_escape_openings, text_escape_closures, strip_leading_and_tailing_whitespace, encoding, newline_offsets, fields)","text":"

PARALLEL TASK FUNCTION reads columnsname + path[start:limit] into hdf5.

source: csv or txt file destination: filename for page. start: int: start of page. end: int: end of page. guess_datatypes: bool: if True datatypes will be inferred by datatypes.Datatypes.guess delimiter: ',' ';' or '|' text_qualifier: str: commonly \" text_escape_openings: str: default: \"({[ text_escape_closures: str: default: ]})\" strip_leading_and_tailing_whitespace: bool encoding: chardet encoding ('utf-8, 'ascii', ..., 'ISO-22022-CN')

Source code in tablite/import_utils.py
def text_reader_task(\n    source,\n    destination,\n    start,\n    end,\n    guess_datatypes,\n    delimiter,\n    text_qualifier,\n    text_escape_openings,\n    text_escape_closures,\n    strip_leading_and_tailing_whitespace,\n    encoding,\n    newline_offsets,\n    fields\n):\n    \"\"\"PARALLEL TASK FUNCTION\n    reads columnsname + path[start:limit] into hdf5.\n\n    source: csv or txt file\n    destination: filename for page.\n    start: int: start of page.\n    end: int: end of page.\n    guess_datatypes: bool: if True datatypes will be inferred by datatypes.Datatypes.guess\n    delimiter: ',' ';' or '|'\n    text_qualifier: str: commonly \\\"\n    text_escape_openings: str: default: \"({[\n    text_escape_closures: str: default: ]})\"\n    strip_leading_and_tailing_whitespace: bool\n    encoding: chardet encoding ('utf-8, 'ascii', ..., 'ISO-22022-CN')\n    \"\"\"\n    if isinstance(source, str):\n        source = Path(source)\n    type_check(source, Path)\n    if not source.exists():\n        raise FileNotFoundError(f\"File not found: {source}\")\n    type_check(destination, list)\n\n    # declare CSV dialect.\n    delim = delimiter\n\n    class Dialect(csv.Dialect):\n        delimiter = delim\n        quotechar = '\"' if text_qualifier is None else text_qualifier\n        escapechar = '\\\\'\n        doublequote = True\n        quoting = csv.QUOTE_MINIMAL\n        skipinitialspace = False if strip_leading_and_tailing_whitespace is None else strip_leading_and_tailing_whitespace\n        lineterminator = \"\\n\"\n\n    with source.open(\"r\", encoding=encoding, errors=\"ignore\") as fi:  # --READ\n        fi.seek(newline_offsets[start])\n        reader = csv.reader(fi, dialect=Dialect)\n\n        # if there's an issue with file handlers on windows, we can make a special case for windows where the file is opened on demand and appended instead of opening all handlers at once\n        page_file_handlers = [open(f, mode=\"wb\") for f in destination]\n\n        # identify longest str\n        longest_str = [1 for _ in range(len(destination))]\n        for row in (next(reader) for _ in range(end - start)):\n            for idx, c in ((fields[idx], c) for idx, c in filter(lambda t: t[0] in fields, enumerate(row))):\n                longest_str[idx] = max(longest_str[idx], len(c))\n\n        column_formats = [f\"<U{i}\" for i in longest_str]\n        for idx, cf in enumerate(column_formats):\n            _create_numpy_header(cf, (end - start, ), page_file_handlers[idx])\n\n        # write page arrays to files\n        fi.seek(newline_offsets[start])\n        for row in (next(reader) for _ in range(end - start)):\n            for idx, c in ((fields[idx], c) for idx, c in filter(lambda t: t[0] in fields, enumerate(row))):\n                cbytes = np.asarray(c, dtype=column_formats[idx]).tobytes()\n                page_file_handlers[idx].write(cbytes)\n\n        [phf.close() for phf in page_file_handlers]\n
"},{"location":"reference/import_utils/#tablite.import_utils.text_reader","title":"tablite.import_utils.text_reader(T, path, columns, first_row_has_headers, header_row_index, encoding, start, limit, newline, guess_datatypes, text_qualifier, strip_leading_and_tailing_whitespace, delimiter, text_escape_openings, text_escape_closures, tqdm=_tqdm, **kwargs)","text":"Source code in tablite/import_utils.py
def text_reader(\n    T,\n    path,\n    columns,\n    first_row_has_headers,\n    header_row_index,\n    encoding,\n    start,\n    limit,\n    newline,\n    guess_datatypes,\n    text_qualifier,\n    strip_leading_and_tailing_whitespace,\n    delimiter,\n    text_escape_openings,\n    text_escape_closures,\n    tqdm=_tqdm,\n    **kwargs,\n):\n    if encoding is None:\n        encoding = get_encoding(path, nbytes=ENCODING_GUESS_BYTES)\n\n    if encoding.lower() in [\"utf8\", \"utf-8\", \"utf-8-sig\"]:\n        enc = \"ENC_UTF8\"\n    elif encoding.lower() in [\"utf16\", \"utf-16\"]:\n        enc = \"ENC_UTF16\"\n    elif encoding in Config.NIM_SUPPORTED_CONV_TYPES:\n        enc = f\"ENC_CONV|{encoding}\"\n    else:\n        raise NotImplementedError(f\"encoding not implemented: {encoding}\")\n\n    pid = Config.workdir / Config.pid\n    kwargs = {}\n\n    if first_row_has_headers is not None:\n        kwargs[\"first_row_has_headers\"] = first_row_has_headers\n    if header_row_index is not None:\n        kwargs[\"header_row_index\"] = header_row_index\n    if columns is not None:\n        kwargs[\"columns\"] = columns\n    if start is not None:\n        kwargs[\"start\"] = start\n    if limit is not None and limit != sys.maxsize:\n        kwargs[\"limit\"] = limit\n    if guess_datatypes is not None:\n        kwargs[\"guess_datatypes\"] = guess_datatypes\n    if newline is not None:\n        kwargs[\"newline\"] = newline\n    if delimiter is not None:\n        kwargs[\"delimiter\"] = delimiter\n    if text_qualifier is not None:\n        kwargs[\"text_qualifier\"] = text_qualifier\n        kwargs[\"quoting\"] = \"QUOTE_MINIMAL\"\n    else:\n        kwargs[\"quoting\"] = \"QUOTE_NONE\"\n    if strip_leading_and_tailing_whitespace is not None:\n        kwargs[\"strip_leading_and_tailing_whitespace\"] = strip_leading_and_tailing_whitespace\n\n    return nimlite.text_reader(\n        T, pid, path, enc,\n        **kwargs,\n        tqdm=tqdm\n    )\n
"},{"location":"reference/import_utils/#tablite.import_utils-modules","title":"Modules","text":""},{"location":"reference/imputation/","title":"Imputation","text":""},{"location":"reference/imputation/#tablite.imputation","title":"tablite.imputation","text":""},{"location":"reference/imputation/#tablite.imputation-classes","title":"Classes","text":""},{"location":"reference/imputation/#tablite.imputation-functions","title":"Functions","text":""},{"location":"reference/imputation/#tablite.imputation.imputation","title":"tablite.imputation.imputation(T, targets, missing=None, method='carry forward', sources=None, tqdm=_tqdm, pbar=None)","text":"

In statistics, imputation is the process of replacing missing data with substituted values.

See more: https://en.wikipedia.org/wiki/Imputation_(statistics)

PARAMETER DESCRIPTION table

source table.

TYPE: Table

targets

column names to find and replace missing values

TYPE: str or list of strings

missing

values to be replaced.

TYPE: None or iterable DEFAULT: None

method

method to be used for replacement. Options:

'carry forward': takes the previous value, and carries forward into fields where values are missing. +: quick. Realistic on time series. -: Can produce strange outliers.

'mean': calculates the column mean (exclude missing) and copies the mean in as replacement. +: quick -: doesn't work on text. Causes data set to drift towards the mean.

'mode': calculates the column mode (exclude missing) and copies the mean in as replacement. +: quick -: most frequent value becomes over-represented in the sample

'nearest neighbour': calculates normalised distance between items in source columns selects nearest neighbour and copies value as replacement. +: works for any datatype. -: computationally intensive (e.g. slow)

TYPE: str DEFAULT: 'carry forward'

sources

NEAREST NEIGHBOUR ONLY column names to be used during imputation. if None or empty, all columns will be used.

TYPE: list of strings DEFAULT: None

RETURNS DESCRIPTION table

table with replaced values.

Source code in tablite/imputation.py
def imputation(T, targets, missing=None, method=\"carry forward\", sources=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    In statistics, imputation is the process of replacing missing data with substituted values.\n\n    See more: https://en.wikipedia.org/wiki/Imputation_(statistics)\n\n    Args:\n        table (Table): source table.\n\n        targets (str or list of strings): column names to find and\n            replace missing values\n\n        missing (None or iterable): values to be replaced.\n\n        method (str): method to be used for replacement. Options:\n\n            'carry forward':\n                takes the previous value, and carries forward into fields\n                where values are missing.\n                +: quick. Realistic on time series.\n                -: Can produce strange outliers.\n\n            'mean':\n                calculates the column mean (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: doesn't work on text. Causes data set to drift towards the mean.\n\n            'mode':\n                calculates the column mode (exclude `missing`) and copies\n                the mean in as replacement.\n                +: quick\n                -: most frequent value becomes over-represented in the sample\n\n            'nearest neighbour':\n                calculates normalised distance between items in source columns\n                selects nearest neighbour and copies value as replacement.\n                +: works for any datatype.\n                -: computationally intensive (e.g. slow)\n\n        sources (list of strings): NEAREST NEIGHBOUR ONLY\n            column names to be used during imputation.\n            if None or empty, all columns will be used.\n\n    Returns:\n        table: table with replaced values.\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if isinstance(targets, str) and targets not in T.columns:\n        targets = [targets]\n    if isinstance(targets, list):\n        for name in targets:\n            if not isinstance(name, str):\n                raise TypeError(f\"expected str, not {type(name)}\")\n            if name not in T.columns:\n                raise ValueError(f\"target item {name} not a column name in T.columns:\\n{T.columns}\")\n    else:\n        raise TypeError(\"Expected source as list of column names\")\n\n    if missing is None:\n        missing = {None}\n    else:\n        missing = set(missing)\n\n    if method == \"nearest neighbour\":\n        if sources in (None, []):\n            sources = list(T.columns)\n        if isinstance(sources, str):\n            sources = [sources]\n        if isinstance(sources, list):\n            for name in sources:\n                if not isinstance(name, str):\n                    raise TypeError(f\"expected str, not {type(name)}\")\n                if name not in T.columns:\n                    raise ValueError(f\"source item {name} not a column name in T.columns:\\n{T.columns}\")\n        else:\n            raise TypeError(\"Expected source as list of column names\")\n\n    methods = [\"nearest neighbour\", \"mean\", \"mode\", \"carry forward\"]\n\n    if method == \"carry forward\":\n        return carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None)\n    elif method in {\"mean\", \"mode\"}:\n        return stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None)\n    elif method == \"nearest neighbour\":\n        return nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None)\n    else:\n        raise ValueError(f\"method {method} not recognised amonst known methods: {list(methods)})\")\n
"},{"location":"reference/imputation/#tablite.imputation.carry_forward","title":"tablite.imputation.carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def carry_forward(T, targets, missing, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    if pbar is None:\n        total = len(targets) * len(T)\n        pbar = tqdm(total=total, desc=\"imputation.carry_forward\", disable=Config.TQDM_DISABLE)\n\n    new = type(T)()\n    for name in T.columns:\n        if name in targets:\n            data = T[name][:]  # create copy\n            last_value = None\n            for ix, v in enumerate(data):\n                if v in missing:  # perform replacement\n                    data[ix] = last_value\n                else:  # keep last value.\n                    last_value = v\n                pbar.update(1)\n            new[name] = data\n        else:\n            new[name] = T[name]\n\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation.stats_method","title":"tablite.imputation.stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def stats_method(T, targets, missing, method, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    if pbar is None:\n        total = len(targets)\n        pbar = tqdm(total=total, desc=f\"imputation.{method}\", disable=Config.TQDM_DISABLE)\n\n    new = type(T)()\n    for name in T.columns:\n        if name in targets:\n            col = T.columns[name]\n            assert isinstance(col, Column)\n            stats = col.statistics()\n            new_value = stats[method]\n            col.replace(mapping={m: new_value for m in missing})\n            new[name] = col\n            pbar.update(1)\n        else:\n            new[name] = T[name]  # no entropy, keep as is.\n\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation.nearest_neighbour","title":"tablite.imputation.nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/imputation.py
def nearest_neighbour(T, sources, missing, targets, tqdm=_tqdm, pbar=None):\n    assert isinstance(missing, set)\n\n    new = T.copy()\n    norm_index = {}\n    normalised_values = Table()\n    for name in sources:\n        values = T[name].unique().tolist()\n        values = sort_utils.unix_sort(values, reverse=False)\n        values = [(v, k) for k, v in values.items()]\n        values.sort()\n        values = [k for _, k in values]\n\n        n = len([v for v in values if v not in missing])\n        d = {v: i / n if v not in missing else math.inf for i, v in enumerate(values)}\n        normalised_values[name] = [d[v] for v in T[name]]\n        norm_index[name] = d\n        values.clear()\n\n    missing_value_index = T.index(*targets)\n    missing_value_index = {k: v for k, v in missing_value_index.items() if missing.intersection(set(k))}  # strip out all that do not have missings.\n\n    ranks = set()\n    for k, v in missing_value_index.items():\n        ranks.update(set(k))\n    item_order = sort_utils.unix_sort(list(ranks))\n    new_order = {tuple(item_order[i] for i in k): k for k in missing_value_index.keys()}\n\n    if pbar is None:\n        total = total=sum(len(v) for v in missing_value_index.values())\n        pbar = tqdm(total=total, desc=f\"imputation.nearest_neighbour\", disable=Config.TQDM_DISABLE)\n\n    for _, key in sorted(new_order.items(), reverse=True):  # Fewest None's are at the front of the list.\n        for row_id in missing_value_index[key]:\n            err_map = [0.0 for _ in range(len(T))]\n            for n, v in T.to_dict(columns=sources, slice_=slice(row_id, row_id + 1, 1)).items():\n                # ^--- T.to_dict doesn't go to disk as hence saves an IO step.\n                v = v[0]\n                norm_value = norm_index[n][v]\n                if norm_value != math.inf:\n                    err_map = [e1 + abs(norm_value - e2) for e1, e2 in zip(err_map, normalised_values[n])]\n\n            min_err = min(err_map)\n            ix = err_map.index(min_err)\n\n            for name in targets:\n                current_value = new[name][row_id]\n                if current_value not in missing:  # no need to replace anything.\n                    continue\n                if new[name][ix] not in missing:  # can confidently impute.\n                    new[name][row_id] = new[name][ix]\n                else:  # replacement is required, but ix points to another missing value.\n                    # we therefore have to search after the next best match:\n                    tmp_err_map = err_map[:]\n                    for _ in range(len(err_map)):\n                        tmp_min_err = min(tmp_err_map)\n                        tmp_ix = tmp_err_map.index(tmp_min_err)\n                        if row_id == tmp_ix:\n                            tmp_err_map[tmp_ix] = math.inf\n                            continue\n                        elif new[name][tmp_ix] in missing:\n                            tmp_err_map[tmp_ix] = math.inf\n                            continue\n                        else:\n                            new[name][row_id] = new[name][tmp_ix]\n                            break\n\n            pbar.update(1)\n    return new\n
"},{"location":"reference/imputation/#tablite.imputation-modules","title":"Modules","text":""},{"location":"reference/joins/","title":"Joins","text":""},{"location":"reference/joins/#tablite.joins","title":"tablite.joins","text":""},{"location":"reference/joins/#tablite.joins-classes","title":"Classes","text":""},{"location":"reference/joins/#tablite.joins-functions","title":"Functions","text":""},{"location":"reference/joins/#tablite.joins.join","title":"tablite.joins.join(T, other, left_keys, right_keys, left_columns, right_columns, kind='inner', tqdm=_tqdm, pbar=None)","text":"

short-cut for all join functions.

PARAMETER DESCRIPTION T

left table

TYPE: Table

other

right table

TYPE: Table

left_keys

list of keys for the join from left table.

TYPE: list

right_keys

list of keys for the join from right table.

TYPE: list

left_columns

list of columns names to retain from left table. If None, all are retained.

TYPE: list

right_columns

list of columns names to retain from right table. If None, all are retained.

TYPE: list

kind

'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".

TYPE: str DEFAULT: 'inner'

tqdm

tqdm progress counter. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

tqdm.progressbar. Defaults to None.

TYPE: pbar DEFAULT: None

RAISES DESCRIPTION ValueError

if join type is unknown.

RETURNS DESCRIPTION Table

joined table.

Source code in tablite/joins.py
def join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", tqdm=_tqdm, pbar=None):\n    \"\"\"short-cut for all join functions.\n\n    Args:\n        T (Table): left table\n        other (Table): right table\n        left_keys (list): list of keys for the join from left table.\n        right_keys (list): list of keys for the join from right table.\n        left_columns (list): list of columns names to retain from left table. \n            If None, all are retained.\n        right_columns (list): list of columns names to retain from right table. \n            If None, all are retained.\n        kind (str, optional): 'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".\n        tqdm (tqdm, optional): tqdm progress counter. Defaults to _tqdm.\n        pbar (tqdm.pbar, optional): tqdm.progressbar. Defaults to None.\n\n    Raises:\n        ValueError: if join type is unknown.\n\n    Returns:\n        Table: joined table.\n    \"\"\"\n    kinds = {\n        \"inner\": T.inner_join,\n        \"left\": T.left_join,\n        \"outer\": T.outer_join,\n        \"cross\": T.cross_join,\n    }\n    if kind not in kinds:\n        raise ValueError(f\"join type unknown: {kind}\")\n    f = kinds.get(kind, None)\n    return f(other, left_keys, right_keys, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/joins/#tablite.joins.left_join","title":"tablite.joins.left_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"PARAMETER DESCRIPTION T

left table

TYPE: Table

other

right table

TYPE: Table

left_keys

list of keys for the join from left table.

TYPE: list

right_keys

list of keys for the join from right table.

TYPE: list

left_columns

list of columns names to retain from left table. If None, all are retained.

TYPE: list DEFAULT: None

right_columns

list of columns names to retain from right table. If None, all are retained.

TYPE: list DEFAULT: None

kind

'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".

TYPE: str

tqdm

tqdm progress counter. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

tqdm.progressbar. Defaults to None.

TYPE: pbar DEFAULT: None

merge_keys

merges keys to the left, so that cases where right key is None, a key exists.

TYPE: boolean DEFAULT: None

RETURNS DESCRIPTION Table

joined table

Example:

SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n

Tablite:

>>> left_join = numbers.left_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n)\n
Source code in tablite/joins.py
def left_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    Args:\n        T (Table): left table\n        other (Table): right table\n        left_keys (list): list of keys for the join from left table.\n        right_keys (list): list of keys for the join from right table.\n        left_columns (list): list of columns names to retain from left table. \n            If None, all are retained.\n        right_columns (list): list of columns names to retain from right table. \n            If None, all are retained.\n        kind (str, optional): 'inner', 'left', 'outer', 'cross'. Defaults to \"inner\".\n        tqdm (tqdm, optional): tqdm progress counter. Defaults to _tqdm.\n        pbar (tqdm.pbar, optional): tqdm.progressbar. Defaults to None.\n        merge_keys (boolean): merges keys to the left, so that cases where right key is None, a key exists.\n\n    Returns: \n        Table: joined table\n\n    Example:\n    ```\n    SQL:   SELECT number, letter FROM numbers LEFT JOIN letters ON numbers.colour == letters.color\n    ```\n    Tablite: \n    ```\n    >>> left_join = numbers.left_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n    )\n    ```\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT = [], []\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, (-1,))\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)  # compress memory of python list to array.\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys is True:\n        boolean_map = (RIGHT == -1)\n        column_names = [n for n in T.columns]  # left side only.\n        for left_name,right_name in zip(left_keys,right_keys):\n            right_name = unique_name(right_name, T.columns)\n            column_names.append(right_name)\n            result = where(result, boolean_map, left_name,right_name,new=left_name)\n    return result\n
"},{"location":"reference/joins/#tablite.joins.inner_join","title":"tablite.joins.inner_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that only left key is present :return: new Table Example: SQL: SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color Tablite: inner_join = numbers.inner_join( letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'] )

Source code in tablite/joins.py
def inner_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that only left key is present\n    :return: new Table\n    Example:\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    Tablite: inner_join = numbers.inner_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT = [], []\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, None)\n        if right_ixs is None:\n            continue\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys:\n        for right_name in right_keys:\n            right_name = unique_name(right_name, T.columns)\n            if right_name in result.columns:\n                del result[right_name]\n    return result\n
"},{"location":"reference/joins/#tablite.joins.outer_join","title":"tablite.joins.outer_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that cases where a key match is None, a key exists. :return: new Table Example: SQL: SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color Tablite: outer_join = numbers.outer_join( letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter'] )

Source code in tablite/joins.py
def outer_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that cases where a key match is None, a key exists.\n    :return: new Table\n    Example:\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    Tablite: outer_join = numbers.outer_join(\n        letters, left_keys=['colour'], right_keys=['color'], left_columns=['number'], right_columns=['letter']\n        )\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    left_index = T.index(*left_keys)\n    right_index = other.index(*right_keys)\n    LEFT, RIGHT, RIGHT_UNUSED = [], [], set(right_index.keys())\n    for left_key, left_ixs in left_index.items():\n        right_ixs = right_index.get(left_key, (-1,))\n        for left_ix in left_ixs:\n            for right_ix in right_ixs:\n                LEFT.append(left_ix)\n                RIGHT.append(right_ix)\n                RIGHT_UNUSED.discard(left_key)\n\n    for right_key in RIGHT_UNUSED:\n        for right_ix in right_index[right_key]:\n            LEFT.append(-1)\n            RIGHT.append(right_ix)\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT) * len(left_columns + right_columns), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys is True:\n        boolean_map = (LEFT != -1)\n        column_names = [n for n in T.columns]\n        for left_name,right_name in zip(left_keys,right_keys):\n            right_name = unique_name(right_name, T.columns)\n            column_names.append(right_name)\n            result = where(result, boolean_map, left_name,right_name,new=left_name)\n    return result\n
"},{"location":"reference/joins/#tablite.joins.cross_join","title":"tablite.joins.cross_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None)","text":"

:param T: Table (left) :param other: Table (right) :param left_keys: list of keys for the join :param right_keys: list of keys for the join :param left_columns: list of left columns to retain, if None, all are retained. :param right_columns: list of right columns to retain, if None, all are retained. :param merge_keys: merges keys, so that only left key is present :return: new Table

CROSS JOIN returns the Cartesian product of rows from tables in the join. In other words, it will produce rows which combine each row from the first table with each row from the second table

Source code in tablite/joins.py
def cross_join(T, other, left_keys, right_keys, left_columns=None, right_columns=None, merge_keys=None, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    :param T: Table (left)\n    :param other: Table (right)\n    :param left_keys: list of keys for the join\n    :param right_keys: list of keys for the join\n    :param left_columns: list of left columns to retain, if None, all are retained.\n    :param right_columns: list of right columns to retain, if None, all are retained.\n    :param merge_keys: merges keys, so that only left key is present\n    :return: new Table\n\n    CROSS JOIN returns the Cartesian product of rows from tables in the join.\n    In other words, it will produce rows which combine each row from the first table\n    with each row from the second table\n    \"\"\"\n    if left_columns is None:\n        left_columns = list(T.columns)\n    if right_columns is None:\n        right_columns = list(other.columns)\n    assert merge_keys in {True,False,None}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    LEFT, RIGHT = zip(*product(range(len(T)), range(len(other))))\n\n    LEFT, RIGHT = np.array(LEFT), np.array(RIGHT)\n    f = select_processing_method(len(LEFT), _sp_join, _mp_join)\n    result = f(T, other, LEFT, RIGHT, left_columns, right_columns, tqdm=tqdm, pbar=pbar)\n\n    if merge_keys:\n        for right_name in right_keys:\n            right_name = unique_name(right_name, T.columns)\n            if right_name in result.columns:\n                del result[right_name]\n    return result\n
"},{"location":"reference/lookup/","title":"Lookup","text":""},{"location":"reference/lookup/#tablite.lookup","title":"tablite.lookup","text":""},{"location":"reference/lookup/#tablite.lookup-attributes","title":"Attributes","text":""},{"location":"reference/lookup/#tablite.lookup-classes","title":"Classes","text":""},{"location":"reference/lookup/#tablite.lookup-functions","title":"Functions","text":""},{"location":"reference/lookup/#tablite.lookup.lookup","title":"tablite.lookup.lookup(T, other, *criteria, all=True, tqdm=_tqdm)","text":"

function for looking up values in other according to criteria in ascending order. :param: T: Table :param: other: Table sorted in ascending search order. :param: criteria: Each criteria must be a tuple with value comparisons in the form: (LEFT, OPERATOR, RIGHT) :param: all: boolean: True=ALL, False=ANY

OPERATOR must be a callable that returns a boolean LEFT must be a value that the OPERATOR can compare. RIGHT must be a value that the OPERATOR can compare.

Examples:

comparison of two columns:

('column A', \"==\", 'column B')\n

compare value from column 'Date' with date 24/12.

('Date', \"<\", DataTypes.date(24,12) )\n

uses custom function to compare value from column 'text 1' with value from column 'text 2'

f = lambda L,R: all( ord(L) < ord(R) )\n('text 1', f, 'text 2')\n
Source code in tablite/lookup.py
def lookup(T, other, *criteria, all=True, tqdm=_tqdm):\n    \"\"\"function for looking up values in `other` according to criteria in ascending order.\n    :param: T: Table \n    :param: other: Table sorted in ascending search order.\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n        (LEFT, OPERATOR, RIGHT)\n    :param: all: boolean: True=ALL, False=ANY\n\n    OPERATOR must be a callable that returns a boolean\n    LEFT must be a value that the OPERATOR can compare.\n    RIGHT must be a value that the OPERATOR can compare.\n\n    Examples:\n        comparison of two columns:\n\n            ('column A', \"==\", 'column B')\n\n        compare value from column 'Date' with date 24/12.\n\n            ('Date', \"<\", DataTypes.date(24,12) )\n\n        uses custom function to compare value from column\n        'text 1' with value from column 'text 2'\n\n            f = lambda L,R: all( ord(L) < ord(R) )\n            ('text 1', f, 'text 2')\n\n    \"\"\"\n    sub_cls_check(T, Table)\n    sub_cls_check(other, Table)\n\n    all = all\n    any = not all\n\n    ops = lookup_ops\n\n    functions, left_criteria, right_criteria = [], set(), set()\n\n    for left, op, right in criteria:\n        left_criteria.add(left)\n        right_criteria.add(right)\n        if callable(op):\n            pass  # it's a custom function.\n        else:\n            op = ops.get(op, None)\n            if not callable(op):\n                raise ValueError(f\"{op} not a recognised operator for comparison.\")\n\n        functions.append((op, left, right))\n    left_columns = [n for n in left_criteria if n in T.columns]\n    right_columns = [n for n in right_criteria if n in other.columns]\n\n    result_index = np.empty(shape=(len(T)), dtype=np.int64)\n    cache = {}\n    left = T[left_columns]\n    if isinstance(left, Column):\n        tmp, left = left, Table()\n        left[left_columns[0]] = tmp\n    right = other[right_columns]\n    if isinstance(right, Column):\n        tmp, right = right, Table()\n        right[right_columns[0]] = tmp\n    assert isinstance(left, Table)\n    assert isinstance(right, Table)\n\n    for ix, row1 in tqdm(enumerate(left.rows), total=len(T), disable=Config.TQDM_DISABLE):\n        row1_tup = tuple(row1)\n        row1d = {name: value for name, value in zip(left_columns, row1)}\n        row1_hash = hash(row1_tup)\n\n        match_found = True if row1_hash in cache else False\n\n        if not match_found:  # search.\n            for row2ix, row2 in enumerate(right.rows):\n                row2d = {name: value for name, value in zip(right_columns, row2)}\n\n                evaluations = {op(row1d.get(left, left), row2d.get(right, right)) for op, left, right in functions}\n                # The evaluations above does a neat trick:\n                # as L is a dict, L.get(left, L) will return a value\n                # from the columns IF left is a column name. If it isn't\n                # the function will treat left as a value.\n                # The same applies to right.\n                all_ = all and (False not in evaluations)\n                any_ = any and True in evaluations\n                if all_ or any_:\n                    match_found = True\n                    cache[row1_hash] = row2ix\n                    break\n\n        if not match_found:  # no match found.\n            cache[row1_hash] = -1  # -1 is replacement for None in the index as numpy can't handle Nones.\n\n        result_index[ix] = cache[row1_hash]\n\n    f = select_processing_method(2 * max(len(T), len(other)), _sp_lookup, _mp_lookup)\n    return f(T, other, result_index)\n
"},{"location":"reference/match/","title":"Match","text":""},{"location":"reference/match/#tablite.match","title":"tablite.match","text":""},{"location":"reference/match/#tablite.match-classes","title":"Classes","text":""},{"location":"reference/match/#tablite.match-functions","title":"Functions","text":""},{"location":"reference/match/#tablite.match.match","title":"tablite.match.match(T, other, *criteria, keep_left=None, keep_right=None)","text":"

performs inner join where T matches other and removes rows that do not match.

:param: T: Table :param: other: Table :param: criteria: Each criteria must be a tuple with value comparisons in the form:

(LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\nExample:\n    ('column A', \"==\", 'column B')\n\nThis syntax follows the lookup syntax. See Lookup for details.\n

:param: keep_left: list of columns to keep. :param: keep_right: list of right columns to keep.

Source code in tablite/match.py
def match(T, other, *criteria, keep_left=None, keep_right=None):  # lookup and filter combined - drops unmatched rows.\n    \"\"\"\n    performs inner join where `T` matches `other` and removes rows that do not match.\n\n    :param: T: Table\n    :param: other: Table\n    :param: criteria: Each criteria must be a tuple with value comparisons in the form:\n\n        (LEFT, OPERATOR, RIGHT), where operator must be \"==\"\n\n        Example:\n            ('column A', \"==\", 'column B')\n\n        This syntax follows the lookup syntax. See Lookup for details.\n\n    :param: keep_left: list of columns to keep.\n    :param: keep_right: list of right columns to keep.\n    \"\"\"\n    assert isinstance(T, Table)\n    assert isinstance(other, Table)\n    if keep_left is None:\n        keep_left = [n for n in T.columns]\n    else:\n        type_check(keep_left, list)\n        name_check(T.columns, *keep_left)\n\n    if keep_right is None:\n        keep_right = [n for n in other.columns]\n    else:\n        type_check(keep_right, list)\n        name_check(other.columns, *keep_right)\n\n    indices = np.full(shape=(len(T),), fill_value=-1, dtype=np.int64)\n    for arg in criteria:\n        b,_,a = arg\n        if _ != \"==\":\n            raise ValueError(\"match requires A == B. For other logic visit `lookup`\")\n        if b not in T.columns:\n            raise ValueError(f\"Column {b} not found in T for criteria: {arg}\")\n        if a not in other.columns:\n            raise ValueError(f\"Column {a} not found in T for criteria: {arg}\")\n\n        index_update = find_indices(other[a][:], T[b][:], fill_value=-1)\n        indices = merge_indices(indices, index_update)\n\n    cls = type(T)\n    new = cls()\n    for name in T.columns:\n        if name in keep_left:\n            new[name] = np.compress(indices != -1, T[name][:])\n\n    for name in other.columns:\n        if name in keep_right:\n            new_name = unique_name(name, new.columns)\n            primary = np.compress(indices != -1, indices)\n            new[new_name] = np.take(other[name][:], primary)\n\n    return new\n
"},{"location":"reference/match/#tablite.match.find_indices","title":"tablite.match.find_indices(x, y, fill_value=-1)","text":"

finds index of y in x

Source code in tablite/match.py
def find_indices(x,y, fill_value=-1):  # fast.\n    \"\"\"\n    finds index of y in x\n    \"\"\"\n    # disassembly of numpy:\n    # import numpy as np\n    # x = np.array([3, 5, 7,  1,   9, 8, 6, 6])\n    # y = np.array([2, 1, 5, 10, 100, 6])\n    index = np.argsort(x)  # array([3, 0, 1, 6, 7, 2, 5, 4])\n    sorted_x = x[index]  # array([1, 3, 5, 6, 6, 7, 8, 9])\n    sorted_index = np.searchsorted(sorted_x, y)  # array([1, 0, 2, 8, 8, 3])\n    yindex = np.take(index, sorted_index, mode=\"clip\")  # array([0, 3, 1, 4, 4, 6])\n    mask = x[yindex] != y  # array([ True, False, False,  True,  True, False])\n    indices = np.ma.array(yindex, mask=mask, fill_value=fill_value)  \n    # masked_array(data=[--, 3, 1, --, --, 6], mask=[ True, False, False,  True,  True, False], fill_value=999999)\n    # --: y[0] not in x\n    # 3 : y[1] == x[3]\n    # 1 : y[2] == x[1]\n    # --: y[3] not in x\n    # --: y[4] not in x\n    # --: y[5] == x[6]\n    result = np.where(~indices.mask, indices.data, -1)  \n    return result  # array([-1,  3,  1, -1, -1,  6])\n
"},{"location":"reference/match/#tablite.match.merge_indices","title":"tablite.match.merge_indices(x1, *args, fill_value=-1)","text":"

merges x1 and x2 where

Source code in tablite/match.py
def merge_indices(x1, *args, fill_value=-1):\n    \"\"\"\n    merges x1 and x2 where \n    \"\"\"\n    # dis:\n    # >>> AA = array([-1,  3, -1, 5])\n    # >>> BB = array([-1, -1,  4, 5])\n    new = x1[:]  # = AA\n    for arg in args:\n        mask = (new == fill_value)  # array([True, False, True, False])\n        new = np.where(mask, arg, new)  # array([-1, 3, 4, 5])\n    return new   # array([-1, 3, 4, 5])\n
"},{"location":"reference/merge/","title":"Merge","text":""},{"location":"reference/merge/#tablite.merge","title":"tablite.merge","text":""},{"location":"reference/merge/#tablite.merge-classes","title":"Classes","text":""},{"location":"reference/merge/#tablite.merge-functions","title":"Functions","text":""},{"location":"reference/merge/#tablite.merge.where","title":"tablite.merge.where(T, criteria, left, right, new)","text":"

takes from LEFT where criteria is True else RIGHT. :param: T: Table :param: criteria: np.array(bool): if True take left column else take right column :param left: (str) column name :param right: (str) column name :param new: (str) new name

:returns: T

Source code in tablite/merge.py
def where(T, criteria, left, right, new):\n    \"\"\" takes from LEFT where criteria is True else RIGHT.\n    :param: T: Table\n    :param: criteria: np.array(bool): \n            if True take left column\n            else take right column\n    :param left: (str) column name\n    :param right: (str) column name\n    :param new: (str) new name\n\n    :returns: T\n    \"\"\"\n    type_check(T, Table)\n    if isinstance(criteria, np.ndarray):\n        if not criteria.dtype == \"bool\":\n            raise TypeError\n    else:\n        criteria = np.array(criteria, dtype='bool')\n\n    new_name = unique_name(new, list(T.columns))\n    T.add_column(new_name)\n    col = T[new_name]\n\n    for start,end in Config.page_steps(len(criteria)):\n        indices = np.arange(start,end)\n        left_values = T[left].get_by_indices(indices)\n        right_values = T[right].get_by_indices(indices)\n        new_values = np.where(criteria, left_values, right_values)\n        col.extend(new_values)\n\n    del T[left]\n    del T[right]\n    if new != new_name and new not in T.columns:\n        T[new] = T[new_name]\n        del T[new_name]\n    return T\n
"},{"location":"reference/mp_utils/","title":"Mp utils","text":""},{"location":"reference/mp_utils/#tablite.mp_utils","title":"tablite.mp_utils","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-attributes","title":"Attributes","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.lookup_ops","title":"tablite.mp_utils.lookup_ops = {'in': _in, 'not in': not_in, '<': operator.lt, '<=': operator.le, '>': operator.gt, '>=': operator.ge, '!=': operator.ne, '==': operator.eq} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.filter_ops","title":"tablite.mp_utils.filter_ops = {'>': operator.gt, '>=': operator.ge, '==': operator.eq, '<': operator.lt, '<=': operator.le, '!=': operator.ne, 'in': _in} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.filter_ops_from_text","title":"tablite.mp_utils.filter_ops_from_text = {'gt': '>', 'gteq': '>=', 'eq': '==', 'lt': '<', 'lteq': '<=', 'neq': '!=', 'in': _in} module-attribute","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-classes","title":"Classes","text":""},{"location":"reference/mp_utils/#tablite.mp_utils-functions","title":"Functions","text":""},{"location":"reference/mp_utils/#tablite.mp_utils.not_in","title":"tablite.mp_utils.not_in(a, b)","text":"Source code in tablite/mp_utils.py
def not_in(a, b):\n    return not operator.contains(str(a), str(b))\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.select_processing_method","title":"tablite.mp_utils.select_processing_method(fields, sp, mp)","text":"PARAMETER DESCRIPTION fields

number of fields

TYPE: int

sp

method for single processing

TYPE: callable

mp

method for multiprocessing

TYPE: callable

RETURNS DESCRIPTION _type_

description

Source code in tablite/mp_utils.py
def select_processing_method(fields, sp, mp):\n    \"\"\"\n\n    Args:\n        fields (int): number of fields\n        sp (callable): method for single processing\n        mp (callable): method for multiprocessing\n\n    Returns:\n        _type_: _description_\n    \"\"\"\n    if Config.MULTIPROCESSING_MODE == Config.FORCE:\n        m = mp\n    elif Config.MULTIPROCESSING_MODE == Config.FALSE:\n        m = sp\n    elif fields < Config.SINGLE_PROCESSING_LIMIT:\n        m = sp\n    elif max(psutil.cpu_count(logical=False), 1) < 2:\n        m = sp\n    else:\n        m = mp\n    return m\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.maskify","title":"tablite.mp_utils.maskify(arr)","text":"Source code in tablite/mp_utils.py
def maskify(arr):\n    none_mask = [False] * len(arr)  # Setting the default\n\n    for i in range(len(arr)):\n        if arr[i] is None:  # Check if our value is None\n            none_mask[i] = True\n            arr[i] = 0  # Remove None from the original array\n\n    return none_mask\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.share_mem","title":"tablite.mp_utils.share_mem(inp_arr, dtype)","text":"Source code in tablite/mp_utils.py
def share_mem(inp_arr, dtype):\n    len_ = len(inp_arr)\n    size = np.dtype(dtype).itemsize * len_\n    shape = (len_,)\n\n    out_shm = shared_memory.SharedMemory(create=True, size=size)  # the co_processors will read this.\n    out_arr_index = np.ndarray(shape, dtype=dtype, buffer=out_shm.buf)\n    out_arr_index[:] = inp_arr\n\n    return out_arr_index, out_shm\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.map_task","title":"tablite.mp_utils.map_task(data_shm_name, index_shm_name, destination_shm_name, shape, dtype, start, end)","text":"Source code in tablite/mp_utils.py
def map_task(data_shm_name, index_shm_name, destination_shm_name, shape, dtype, start, end):\n    # connect\n    shared_data = shared_memory.SharedMemory(name=data_shm_name)\n    data = np.ndarray(shape, dtype=dtype, buffer=shared_data.buf)\n\n    shared_index = shared_memory.SharedMemory(name=index_shm_name)\n    index = np.ndarray(shape, dtype=np.int64, buffer=shared_index.buf)\n\n    shared_target = shared_memory.SharedMemory(name=destination_shm_name)\n    target = np.ndarray(shape, dtype=dtype, buffer=shared_target.buf)\n    # work\n    target[start:end] = np.take(data[start:end], index[start:end])\n    # disconnect\n    shared_data.close()\n    shared_index.close()\n    shared_target.close()\n
"},{"location":"reference/mp_utils/#tablite.mp_utils.reindex_task","title":"tablite.mp_utils.reindex_task(src, dst, index_shm, shm_shape, start, end)","text":"Source code in tablite/mp_utils.py
def reindex_task(src, dst, index_shm, shm_shape, start, end):\n    # connect\n    existing_shm = shared_memory.SharedMemory(name=index_shm)\n    shared_index = np.ndarray(shm_shape, dtype=np.int64, buffer=existing_shm.buf)\n    # work\n    array = load_numpy(src)\n    new = np.take(array, shared_index[start:end])\n    np.save(dst, new, allow_pickle=True, fix_imports=False)\n    # disconnect\n    existing_shm.close()\n
"},{"location":"reference/nimlite/","title":"Nimlite","text":""},{"location":"reference/nimlite/#tablite.nimlite","title":"tablite.nimlite","text":""},{"location":"reference/nimlite/#tablite.nimlite-attributes","title":"Attributes","text":""},{"location":"reference/nimlite/#tablite.nimlite.K","title":"tablite.nimlite.K = TypeVar('K') module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ColumnSelectorDict","title":"tablite.nimlite.ColumnSelectorDict = TypedDict('ColumnSelectorDict', {'column': str, 'type': Union[Literal['int'], Literal['float'], Literal['bool'], Literal['str'], Literal['date'], Literal['time'], Literal['datetime']], 'allow_empty': Union[bool, None], 'rename': Union[str, None]}) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.paths","title":"tablite.nimlite.paths = sys.argv[:] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite-classes","title":"Classes","text":""},{"location":"reference/nimlite/#tablite.nimlite-functions","title":"Functions","text":""},{"location":"reference/nimlite/#tablite.nimlite.text_reader_task","title":"tablite.nimlite.text_reader_task(*, pid, path, encoding, dialect, task, import_fields, guess_dtypes)","text":"Source code in tablite/nimlite.py
def text_reader_task(*, pid, path, encoding, dialect, task, import_fields, guess_dtypes):\n    return nl.text_reader_task(\n        path=path,\n        encoding=encoding,\n        dia_delimiter=dialect[\"delimiter\"],\n        dia_quotechar=dialect[\"quotechar\"],\n        dia_escapechar=dialect[\"escapechar\"],\n        dia_doublequote=dialect[\"doublequote\"],\n        dia_quoting=dialect[\"quoting\"],\n        dia_skipinitialspace=dialect[\"skipinitialspace\"],\n        dia_skiptrailingspace=dialect[\"skiptrailingspace\"],\n        dia_lineterminator=dialect[\"lineterminator\"],\n        dia_strict=dialect[\"strict\"],\n        tsk_pages=task[\"pages\"],\n        tsk_offset=task[\"offset\"],\n        tsk_count=task[\"count\"],\n        import_fields=import_fields,\n        guess_dtypes=guess_dtypes\n    )\n
"},{"location":"reference/nimlite/#tablite.nimlite.text_reader","title":"tablite.nimlite.text_reader(T: Type[Table], pid: str, path: str, encoding: Literal['ENC_UTF8'] | Literal['ENC_UTF16'] | Literal['ENC_WIN1250'] = 'ENC_UTF8', *, first_row_has_headers: bool = True, header_row_index: int = 0, columns: list[str] | None = None, start: int | None = None, limit: int | None = None, guess_datatypes: bool = False, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: str, strip_leading_and_tailing_whitespace: bool = True, tqdm=_tqdm) -> Table","text":"Source code in tablite/nimlite.py
def text_reader(\n    T,\n    pid, path,\n    encoding=\"ENC_UTF8\",\n    *,\n    first_row_has_headers=True, header_row_index=0,\n    columns=None,\n    start=None, limit=None,\n    guess_datatypes=False,\n    newline='\\n', delimiter=',', text_qualifier='\"',\n    quoting, strip_leading_and_tailing_whitespace=True,\n    tqdm=_tqdm\n):\n    assert isinstance(path, Path)\n    assert isinstance(pid, Path)\n\n    table = nl.text_reader(\n        pid=str(pid),\n        path=str(path),\n        encoding=encoding,\n        first_row_has_headers=first_row_has_headers, header_row_index=header_row_index,\n        columns=columns,\n        start=start, limit=limit,\n        guess_datatypes=guess_datatypes,\n        newline=newline, delimiter=delimiter, text_qualifier=text_qualifier,\n        quoting=quoting,\n        strip_leading_and_tailing_whitespace=strip_leading_and_tailing_whitespace,\n        page_size=Config.PAGE_SIZE\n    )\n\n    task_info = table[\"task\"]\n    task_columns = table[\"columns\"]\n\n    ti_path = task_info[\"path\"]\n    ti_encoding = task_info[\"encoding\"]\n    ti_dialect = task_info[\"dialect\"]\n    ti_guess_dtypes = task_info[\"guess_dtypes\"]\n    ti_tasks = task_info[\"tasks\"]\n    ti_import_fields = task_info[\"import_fields\"]\n    ti_import_field_names = task_info[\"import_field_names\"]\n\n    is_windows = platform.system() == \"Windows\"\n    use_logical = False if is_windows else True\n\n    cpus = max(psutil.cpu_count(logical=use_logical), 1)\n\n    tasks = [\n        Task(\n            text_reader_task,\n            path=ti_path,\n            encoding=ti_encoding,\n            dialect=ti_dialect,\n            task=t,\n            guess_dtypes=ti_guess_dtypes,\n            import_fields=ti_import_fields,\n            pid=pid\n        ) for t in ti_tasks\n    ]\n\n    is_sp = False\n\n    if Config.MULTIPROCESSING_MODE == Config.FALSE:\n        is_sp = True\n    elif Config.MULTIPROCESSING_MODE == Config.AUTO and cpus <= 1 or len(tasks) <= 1:\n        is_sp = True\n    elif Config.MULTIPROCESSING_MODE == Config.FORCE:\n        is_sp = False\n\n    if is_sp:\n        res = [\n            task.f(*task.args, **task.kwargs)\n            for task in tqdm(tasks, \"importing file\")\n        ]\n    else:\n        with TaskManager(cpus) as tm:\n            res = tm.execute(tasks, tqdm)\n\n            if not all(isinstance(r, list) for r in res):\n                raise Exception(\"failed\")\n\n    col_path = pid\n    column_dict = {\n        cols: Column(col_path)\n        for cols in ti_import_field_names\n    }\n\n    for res_pages in res:\n        col_map = {\n            n: res_pages[i]\n            for i, n in enumerate(ti_import_field_names)\n        }\n\n        for k, c in column_dict.items():\n            c.pages.append(col_map[k])\n\n    if columns is None:\n        columns = [c[\"name\"] for c in task_columns]\n\n    table_dict = {\n        a[\"name\"]: column_dict[b]\n        for a, b in zip(task_columns, columns)\n    }\n\n    table = T(columns=table_dict)\n\n    return table\n
"},{"location":"reference/nimlite/#tablite.nimlite.wrap","title":"tablite.nimlite.wrap(str_)","text":"Source code in tablite/nimlite.py
def wrap(str_):\n    return '\"' + str_.replace('\"', '\\\\\"').replace(\"'\", \"\\\\'\").replace(\"\\n\", \"\\\\n\").replace(\"\\t\", \"\\\\t\") + '\"'\n
"},{"location":"reference/nimlite/#tablite.nimlite.collect_cs_info","title":"tablite.nimlite.collect_cs_info(i: int, columns: dict, res_cols_pass: list, res_cols_fail: list)","text":"Source code in tablite/nimlite.py
def collect_cs_info(i: int, columns: dict, res_cols_pass: list, res_cols_fail: list):\n    el = {\n        k: column[i]\n        for k, column in columns.items()\n    }\n\n    col_pass = res_cols_pass[i]\n    col_fail = res_cols_fail[i]\n\n    return el, col_pass, col_fail\n
"},{"location":"reference/nimlite/#tablite.nimlite.column_select","title":"tablite.nimlite.column_select(table: K, cols: list[ColumnSelectorDict], tqdm=_tqdm, TaskManager=TaskManager) -> tuple[K, K]","text":"Source code in tablite/nimlite.py
def column_select(table, cols, tqdm=_tqdm, TaskManager=TaskManager):\n    with tqdm(total=100, desc=\"column select\", bar_format='{desc}: {percentage:.1f}%|{bar}{r_bar}') as pbar:\n        T = type(table)\n        dir_pid = Config.workdir / Config.pid\n\n        columns, page_count, is_correct_type, desired_column_map, passed_column_data, failed_column_data, res_cols_pass, res_cols_fail, column_names, reject_reason_name = nl.collect_column_select_info(table, cols, str(dir_pid), pbar)\n\n        if all(is_correct_type.values()):\n            tbl_pass_columns = {\n                desired_name: table[desired_info[0]]\n                for desired_name, desired_info in desired_column_map.items()\n            }\n\n            tbl_fail_columns = {\n                desired_name: []\n                for desired_name in failed_column_data\n            }\n\n            tbl_pass = T(columns=tbl_pass_columns)\n            tbl_fail = T(columns=tbl_fail_columns)\n\n            return (tbl_pass, tbl_fail)\n\n        task_list_inp = (\n            collect_cs_info(i, columns, res_cols_pass, res_cols_fail)\n            for i in range(page_count)\n        )\n\n        page_size = Config.PAGE_SIZE\n\n        tasks = (\n            Task(\n                nl.do_slice_convert, str(dir_pid), page_size, columns, reject_reason_name, res_pass, res_fail, desired_column_map, column_names, is_correct_type\n            )\n            for columns, res_pass, res_fail in task_list_inp\n        )\n\n        cpu_count = max(psutil.cpu_count(), 1)\n\n        if Config.MULTIPROCESSING_MODE == Config.FORCE:\n            is_mp = True\n        elif Config.MULTIPROCESSING_MODE == Config.FALSE:\n            is_mp = False\n        elif Config.MULTIPROCESSING_MODE == Config.AUTO:\n            is_multithreaded = cpu_count > 1\n            is_multipage = page_count > 1\n\n            is_mp = is_multithreaded and is_multipage\n\n        tbl_pass = T({k: [] for k in passed_column_data})\n        tbl_fail = T({k: [] for k in failed_column_data})\n\n        converted = []\n        step_size = 45 / max(page_count - 1, 1)\n\n        class WrapUpdate:\n            def update(self, n):\n                pbar.update(n * step_size)\n\n        if is_mp:\n            with TaskManager(cpu_count=cpu_count) as tm:\n                res = tm.execute(list(tasks), pbar=WrapUpdate())\n\n                if any(isinstance(r, str) for r in res):\n                    raise Exception(\"tasks failed\")\n\n                converted.extend(res)\n        else:\n            for task in tasks:\n                res = task.execute()\n\n                if isinstance(res, str):\n                    raise Exception(res)\n\n                converted.append(res)\n                pbar.update(1)\n\n        def extend_table(table, columns):\n            for (col_name, pg) in columns:\n                table[col_name].pages.append(pg)\n\n        for pg_pass, pg_fail in converted:\n            extend_table(tbl_pass, pg_pass)\n            extend_table(tbl_fail, pg_fail)\n\n        pbar.update(pbar.total - pbar.n)\n\n        return tbl_pass, tbl_fail\n
"},{"location":"reference/nimlite/#tablite.nimlite.read_page","title":"tablite.nimlite.read_page(path: str) -> np.ndarray","text":"Source code in tablite/nimlite.py
def read_page(path):\n    return nl.read_page(path)\n
"},{"location":"reference/nimlite/#tablite.nimlite-modules","title":"Modules","text":""},{"location":"reference/pivots/","title":"Pivots","text":""},{"location":"reference/pivots/#tablite.pivots","title":"tablite.pivots","text":""},{"location":"reference/pivots/#tablite.pivots-classes","title":"Classes","text":""},{"location":"reference/pivots/#tablite.pivots-functions","title":"Functions","text":""},{"location":"reference/pivots/#tablite.pivots.pivot","title":"tablite.pivots.pivot(T, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None)","text":"

param: rows: column names to keep as rows param: columns: column names to keep as columns param: functions: aggregation functions from the Groupby class as

example:

>>> t.show()\n+=====+=====+=====+\n|  A  |  B  |  C  |\n| int | int | int |\n+-----+-----+-----+\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n|    1|    1|    6|\n|    1|    2|    5|\n|    2|    3|    4|\n|    2|    4|    3|\n|    3|    5|    2|\n|    3|    6|    1|\n+=====+=====+=====+\n\n>>> t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n>>> t2.show()\n+===+===+========+=====+=====+=====+\n| # | C |function|(A=1)|(A=2)|(A=3)|\n|row|int|  str   |mixed|mixed|mixed|\n+---+---+--------+-----+-----+-----+\n|0  |  6|Sum(B)  |    2|None |None |\n|1  |  5|Sum(B)  |    4|None |None |\n|2  |  4|Sum(B)  |None |    6|None |\n|3  |  3|Sum(B)  |None |    8|None |\n|4  |  2|Sum(B)  |None |None |   10|\n|5  |  1|Sum(B)  |None |None |   12|\n+===+===+========+=====+=====+=====+\n
Source code in tablite/pivots.py
def pivot(T, rows, columns, functions, values_as_rows=True, tqdm=_tqdm, pbar=None):\n    \"\"\"\n    param: rows: column names to keep as rows\n    param: columns: column names to keep as columns\n    param: functions: aggregation functions from the Groupby class as\n\n    example:\n    ```\n    >>> t.show()\n    +=====+=====+=====+\n    |  A  |  B  |  C  |\n    | int | int | int |\n    +-----+-----+-----+\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    |    1|    1|    6|\n    |    1|    2|    5|\n    |    2|    3|    4|\n    |    2|    4|    3|\n    |    3|    5|    2|\n    |    3|    6|    1|\n    +=====+=====+=====+\n\n    >>> t2 = t.pivot(rows=['C'], columns=['A'], functions=[('B', gb.sum)])\n    >>> t2.show()\n    +===+===+========+=====+=====+=====+\n    | # | C |function|(A=1)|(A=2)|(A=3)|\n    |row|int|  str   |mixed|mixed|mixed|\n    +---+---+--------+-----+-----+-----+\n    |0  |  6|Sum(B)  |    2|None |None |\n    |1  |  5|Sum(B)  |    4|None |None |\n    |2  |  4|Sum(B)  |None |    6|None |\n    |3  |  3|Sum(B)  |None |    8|None |\n    |4  |  2|Sum(B)  |None |None |   10|\n    |5  |  1|Sum(B)  |None |None |   12|\n    +===+===+========+=====+=====+=====+\n    ```\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if isinstance(rows, str):\n        rows = [rows]\n    if not all(isinstance(i, str) for i in rows):\n        raise TypeError(f\"Expected rows as a list of column names, not {[i for i in rows if not isinstance(i,str)]}\")\n\n    if isinstance(columns, str):\n        columns = [columns]\n    if not all(isinstance(i, str) for i in columns):\n        raise TypeError(\n            f\"Expected columns as a list of column names, not {[i for i in columns if not isinstance(i, str)]}\"\n        )\n\n    if not isinstance(values_as_rows, bool):\n        raise TypeError(f\"expected sum_on_rows as boolean, not {type(values_as_rows)}\")\n\n    keys = rows + columns\n    assert isinstance(keys, list)\n\n    extra_steps = 2\n\n    if pbar is None:\n        total = extra_steps\n\n        if len(functions) == 0:\n            total = total + len(keys)\n        else:\n            total = total + len(T)\n\n        pbar = tqdm(total=total, desc=\"pivot\")\n\n    grpby = groupby(T, keys, functions, tqdm=tqdm, pbar=pbar)\n\n    if len(grpby) == 0:  # return empty table. This must be a test?\n        pbar.update(extra_steps)\n        return Table()\n\n    # split keys to determine grid dimensions\n    row_key_index = {}\n    col_key_index = {}\n\n    r = len(rows)\n    c = len(columns)\n    g = len(functions)\n\n    records = defaultdict(dict)\n\n    for row in grpby.rows:\n        row_key = tuple(row[:r])\n        col_key = tuple(row[r : r + c])\n        func_key = tuple(row[r + c :])\n\n        if row_key not in row_key_index:\n            row_key_index[row_key] = len(row_key_index)  # Y\n\n        if col_key not in col_key_index:\n            col_key_index[col_key] = len(col_key_index)  # X\n\n        rix = row_key_index[row_key]\n        cix = col_key_index[col_key]\n        if cix in records:\n            if rix in records[cix]:\n                raise ValueError(\"this should be empty.\")\n        records[cix][rix] = func_key\n\n    pbar.update(1)\n    result = type(T)()\n\n    if values_as_rows:  # ---> leads to more rows.\n        # first create all columns left to right\n\n        n = r + 1  # rows keys + 1 col for function values.\n        cols = [[] for _ in range(n)]\n        for row, ix in row_key_index.items():\n            for col_name, f in functions:\n                cols[-1].append(f\"{f.__name__}({col_name})\")\n                for col_ix, v in enumerate(row):\n                    cols[col_ix].append(v)\n\n        for col_name, values in zip(rows + [\"function\"], cols):\n            col_name = unique_name(col_name, result.columns)\n            result[col_name] = values\n        col_length = len(cols[0])\n        cols.clear()\n\n        # then populate the sparse matrix.\n        for col_key, c in col_key_index.items():\n            col_name = \"(\" + \",\".join([f\"{col_name}={value}\" for col_name, value in zip(columns, col_key)]) + \")\"\n            col_name = unique_name(col_name, result.columns)\n            L = [None for _ in range(col_length)]\n            for r, funcs in records[c].items():\n                for ix, f in enumerate(funcs):\n                    L[g * r + ix] = f\n            result[col_name] = L\n\n    else:  # ---> leads to more columns.\n        n = r\n        cols = [[] for _ in range(n)]\n        for row in row_key_index:\n            for col_ix, v in enumerate(row):\n                cols[col_ix].append(v)  # write key columns.\n\n        for col_name, values in zip(rows, cols):\n            result[col_name] = values\n\n        col_length = len(row_key_index)\n\n        # now populate the sparse matrix.\n        for col_key, c in col_key_index.items():  # select column.\n            cols, names = [], []\n\n            for f, v in zip(functions, func_key):\n                agg_col, func = f\n                terms = \",\".join([agg_col] + [f\"{col_name}={value}\" for col_name, value in zip(columns, col_key)])\n                col_name = f\"{func.__name__}({terms})\"\n                col_name = unique_name(col_name, result.columns)\n                names.append(col_name)\n                cols.append([None for _ in range(col_length)])\n            for r, funcs in records[c].items():\n                for ix, f in enumerate(funcs):\n                    cols[ix][r] = f\n            for name, col in zip(names, cols):\n                result[name] = col\n\n    pbar.update(1)\n\n    return result\n
"},{"location":"reference/pivots/#tablite.pivots.transpose","title":"tablite.pivots.transpose(T, tqdm=_tqdm)","text":"

performs a CCW matrix rotation of the table.

Source code in tablite/pivots.py
def transpose(T, tqdm=_tqdm):\n    \"\"\"performs a CCW matrix rotation of the table.\"\"\"\n    sub_cls_check(T, Table)\n\n    if len(T.columns) == 0:\n        return type(T)()\n\n    assert isinstance(T, Table)\n    new = type(T)()\n    L = list(T.columns)\n    new[L[0]] = L[1:]\n    for row in tqdm(T.rows, desc=\"table transpose\", total=len(T)):\n        new[row[0]] = row[1:]\n    return new\n
"},{"location":"reference/pivots/#tablite.pivots.pivot_transpose","title":"tablite.pivots.pivot_transpose(T, columns, keep=None, column_name='transpose', value_name='value', tqdm=_tqdm)","text":"

Transpose a selection of columns to rows.

PARAMETER DESCRIPTION columns

column names to transpose

TYPE: list of column names

keep

column names to keep (repeat)

TYPE: list of column names DEFAULT: None

RETURNS DESCRIPTION Table

with columns transposed to rows

Example

transpose columns 1,2 and 3 and transpose the remaining columns, except sum.

Input:

| col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n|------|------|------|-----|-----|-----|-----|-----|------|\n| 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n| 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n| ...  |      |      |     |     |     |     |     |      |\n\n>>> t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\nOutput:\n|col1| col2| col3| transpose| value|\n|----|-----|-----|----------|------|\n|1234| 2345| 3456| sun      |   456|\n|1234| 2345| 3456| mon      |   567|\n|1244| 2445| 4456| mon      |     7|\n
Source code in tablite/pivots.py
def pivot_transpose(T, columns, keep=None, column_name=\"transpose\", value_name=\"value\", tqdm=_tqdm):\n    \"\"\"Transpose a selection of columns to rows.\n\n    Args:\n        columns (list of column names): column names to transpose\n        keep (list of column names): column names to keep (repeat)\n\n    Returns:\n        Table: with columns transposed to rows\n\n    Example:\n        transpose columns 1,2 and 3 and transpose the remaining columns, except `sum`.\n\n    Input:\n    ```\n    | col1 | col2 | col3 | sun | mon | tue | ... | sat | sum  |\n    |------|------|------|-----|-----|-----|-----|-----|------|\n    | 1234 | 2345 | 3456 | 456 | 567 |     | ... |     | 1023 |\n    | 1244 | 2445 | 4456 |     |   7 |     | ... |     |    7 |\n    | ...  |      |      |     |     |     |     |     |      |\n\n    >>> t.transpose(keep=[col1, col2, col3], transpose=[sun,mon,tue,wed,thu,fri,sat])`\n\n    Output:\n    |col1| col2| col3| transpose| value|\n    |----|-----|-----|----------|------|\n    |1234| 2345| 3456| sun      |   456|\n    |1234| 2345| 3456| mon      |   567|\n    |1244| 2445| 4456| mon      |     7|\n    ```\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if not isinstance(columns, list):\n        raise TypeError\n\n    for i in columns:\n        if not isinstance(i, str):\n            raise TypeError\n        if i not in T.columns:\n            raise ValueError\n        if columns.count(i)>1:\n            raise ValueError(f\"Column {i} appears more than once\")\n\n    if keep is None:\n        keep = []\n    for i in keep:\n        if not isinstance(i, str):\n            raise TypeError\n        if i not in T.columns:\n            raise ValueError\n\n    if column_name in keep + columns:\n        column_name = unique_name(column_name, set_of_names=keep + columns)\n    if value_name in keep + columns + [column_name]:\n        value_name = unique_name(value_name, set_of_names=keep + columns)\n\n    new = type(T)()\n    new.add_columns(*keep + [column_name, value_name])\n    news = {name: [] for name in new.columns}\n\n    n = len(keep)\n\n    with tqdm(total=len(T), desc=\"transpose\", disable=Config.TQDM_DISABLE) as pbar:\n        for ix, row in enumerate(T[keep + columns].rows, start=1):\n            keeps = row[:n]\n            transposes = row[n:]\n\n            for name, value in zip(keep, keeps):\n                news[name].extend([value] * len(transposes))\n            for name, value in zip(columns, transposes):\n                news[column_name].append(name)\n                news[value_name].append(value)\n\n            if ix % Config.SINGLE_PROCESSING_LIMIT == 0:\n                for name, values in news.items():\n                    new[name].extend(values)\n                    values.clear()\n\n            pbar.update(1)\n\n    for name, values in news.items():\n        new[name].extend(np.array(values))\n        values.clear()\n    return new\n
"},{"location":"reference/redux/","title":"Redux","text":""},{"location":"reference/redux/#tablite.redux","title":"tablite.redux","text":""},{"location":"reference/redux/#tablite.redux-attributes","title":"Attributes","text":""},{"location":"reference/redux/#tablite.redux-classes","title":"Classes","text":""},{"location":"reference/redux/#tablite.redux-functions","title":"Functions","text":""},{"location":"reference/redux/#tablite.redux.filter_all","title":"tablite.redux.filter_all(T, **kwargs)","text":"

returns Table for rows where ALL kwargs match :param kwargs: dictionary with headers and values / boolean callable

Examples:

t = Table()\nt['a'] = [1,2,3,4]\nt['b'] = [10,20,30,40]\n\ndef f(x):\n    return x == 4\ndef g(x):\n    return x < 20\n\nt2 = t.any( **{\"a\":f, \"b\":g})\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\nt2 = t.any(a=f,b=g)\nassert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\ndef h(x):\n    return x>=2\n\ndef i(x):\n    return x<=30\n\nt2 = t.all(a=h,b=i)\nassert [r for r in t2.rows] == [[2,20], [3, 30]]\n
Source code in tablite/redux.py
def filter_all(T, **kwargs):\n    \"\"\"\n    returns Table for rows where ALL kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n\n    Examples:\n\n        t = Table()\n        t['a'] = [1,2,3,4]\n        t['b'] = [10,20,30,40]\n\n        def f(x):\n            return x == 4\n        def g(x):\n            return x < 20\n\n        t2 = t.any( **{\"a\":f, \"b\":g})\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        t2 = t.any(a=f,b=g)\n        assert [r for r in t2.rows] == [[1, 10], [4, 40]]\n\n        def h(x):\n            return x>=2\n\n        def i(x):\n            return x<=30\n\n        t2 = t.all(a=h,b=i)\n        assert [r for r in t2.rows] == [[2,20], [3, 30]]\n\n\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    if not isinstance(kwargs, dict):\n        raise TypeError(\"did you forget to add the ** in front of your dict?\")\n    if not all([k in T.columns for k in kwargs]):\n        raise ValueError(f\"Unknown column(s): {[k for k in kwargs if k not in T.columns]}\")\n\n    mask = np.full((len(T),), True)\n    for k, v in kwargs.items():\n        col = T[k]\n        for start, end, data in col.iter_by_page():\n            if callable(v):\n                vf = np.frompyfunc(v, 1, 1)\n                mask[start:end] = mask[start:end] & np.apply_along_axis(vf, 0, data)\n            else:\n                mask[start:end] = mask[start:end] & (data == v)\n\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.drop","title":"tablite.redux.drop(T, *args)","text":"

drops all rows that contain args

PARAMETER DESCRIPTION T

TYPE: Table

Source code in tablite/redux.py
def drop(T, *args):\n    \"\"\"drops all rows that contain args\n\n    Args:\n        T (Table):\n    \"\"\"\n    sub_cls_check(T, Table)\n    mask = np.full((len(T),), False)\n    for name in T.columns:\n        col = T[name]\n        for start, end, data in col.iter_by_page():\n            for arg in args:\n                mask[start:end] = mask[start:end] | (data == arg)\n\n    mask = np.invert(mask)\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.filter_any","title":"tablite.redux.filter_any(T, **kwargs)","text":"

returns Table for rows where ANY kwargs match :param kwargs: dictionary with headers and values / boolean callable

Source code in tablite/redux.py
def filter_any(T, **kwargs):\n    \"\"\"\n    returns Table for rows where ANY kwargs match\n    :param kwargs: dictionary with headers and values / boolean callable\n    \"\"\"\n    sub_cls_check(T, Table)\n    if not isinstance(kwargs, dict):\n        raise TypeError(\"did you forget to add the ** in front of your dict?\")\n\n    mask = np.full((len(T),), False)\n    for k, v in kwargs.items():\n        col = T[k]\n        for start, end, data in col.iter_by_page():\n            if callable(v):\n                vf = np.frompyfunc(v, 1, 1)\n                mask[start:end] = mask[start:end] | np.apply_along_axis(vf, 0, data)\n            else:\n                mask[start:end] = mask[start:end] | (v == data)\n\n    return _compress_one(T, mask)\n
"},{"location":"reference/redux/#tablite.redux.filter","title":"tablite.redux.filter(T, expressions, filter_type='all', tqdm=_tqdm)","text":"

filters table

PARAMETER DESCRIPTION T

Table.

TYPE: Table subclass

expressions

str: filters based on an expression, such as: \"all((A==B, C!=4, 200<D))\" which is interpreted using python's compiler to:

def _f(A,B,C,D):\n    return all((A==B, C!=4, 200<D))\n

list of dicts: (example):

L = [ {'column1':'A', 'criteria': \"==\", 'column2': 'B'}, {'column1':'C', 'criteria': \"!=\", \"value2\": '4'}, {'value1': 200, 'criteria': \"<\", column2: 'D' } ]

TYPE: list or str

accepted

'column1', 'column2', 'criteria', 'value1', 'value2'

TYPE: dictionary keys

filter_type

Ignored if expressions is str. 'all' or 'any'. Defaults to \"all\".

TYPE: str DEFAULT: 'all'

tqdm

progressbar. Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

RETURNS DESCRIPTION 2xTables

trues, falses

Source code in tablite/redux.py
def filter(T, expressions, filter_type=\"all\", tqdm=_tqdm):\n    \"\"\"filters table\n\n\n    Args:\n        T (Table subclass): Table.\n        expressions (list or str):\n            str:\n                filters based on an expression, such as:\n                \"all((A==B, C!=4, 200<D))\"\n                which is interpreted using python's compiler to:\n\n                def _f(A,B,C,D):\n                    return all((A==B, C!=4, 200<D))\n\n            list of dicts: (example):\n\n            L = [\n                {'column1':'A', 'criteria': \"==\", 'column2': 'B'},\n                {'column1':'C', 'criteria': \"!=\", \"value2\": '4'},\n                {'value1': 200, 'criteria': \"<\", column2: 'D' }\n            ]\n\n        accepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'\n\n        filter_type (str, optional): Ignored if expressions is str.\n            'all' or 'any'. Defaults to \"all\".\n        tqdm (tqdm, optional): progressbar. Defaults to _tqdm.\n\n    Returns:\n        2xTables: trues, falses\n    \"\"\"\n    # determine method\n    sub_cls_check(T, Table)\n    if len(T) == 0:\n        return T.copy(), T.copy()\n\n    if isinstance(expressions, str):\n        mask = _filter_using_expression(T, expressions)\n    elif isinstance(expressions, list):\n        mask = _filter_using_list_of_dicts(T, expressions, filter_type, tqdm)\n    else:\n        raise TypeError\n    # create new tables\n    return _compress_both(T, mask)\n
"},{"location":"reference/reindex/","title":"Reindex","text":""},{"location":"reference/reindex/#tablite.reindex","title":"tablite.reindex","text":""},{"location":"reference/reindex/#tablite.reindex-classes","title":"Classes","text":""},{"location":"reference/reindex/#tablite.reindex-functions","title":"Functions","text":""},{"location":"reference/reindex/#tablite.reindex.reindex","title":"tablite.reindex.reindex(T, index, names=None, tqdm=_tqdm, pbar=None)","text":"

Constant Memory helper for reindexing pages.

Memory usage is set by datatype and Config.PAGE_SIZE

PARAMETER DESCRIPTION T

subclass of Table

TYPE: Table

index

int64.

TYPE: array

names

list of names from T to reindex.

TYPE: (list, str) DEFAULT: None

tqdm

Defaults to _tqdm.

TYPE: tqdm DEFAULT: tqdm

pbar

Defaults to None.

TYPE: pbar DEFAULT: None

RETURNS DESCRIPTION _type_

description

Source code in tablite/reindex.py
def reindex(T, index, names=None, tqdm=_tqdm, pbar=None):\n    \"\"\"Constant Memory helper for reindexing pages.\n\n    Memory usage is set by datatype and Config.PAGE_SIZE\n\n    Args:\n        T (Table): subclass of Table\n        index (np.array): int64.\n        names (list, str): list of names from T to reindex.\n        tqdm (tqdm, optional): Defaults to _tqdm.\n        pbar (pbar, optional): Defaults to None.\n\n    Returns:\n        _type_: _description_\n    \"\"\"\n    if names is None:\n        names = list(T.columns.keys())\n\n    if pbar is None:\n        total = len(names)\n        pbar = tqdm(total=total, desc=\"join\", disable=Config.TQDM_DISABLE)\n\n    sub_cls_check(T, Table)\n    cls = type(T)\n    result = cls()\n    for name in names:\n        result.add_column(name)\n        col = result[name]\n\n        for start, end in Config.page_steps(len(index)):\n            indices = index[start:end]\n            values = T[name].get_by_indices(indices)\n            # in these values, the index of -1 will be wrong.\n            # so if there is any -1 in the indices, they will\n            # have to be replaced with Nones\n            mask = indices == -1\n            if np.any(mask):\n                nones = np.full(index.shape, fill_value=None)\n                values = np.where(mask, nones, values)\n            col.extend(values)\n        pbar.update(1)\n\n    return result\n
"},{"location":"reference/sort_utils/","title":"Sort utils","text":""},{"location":"reference/sort_utils/#tablite.sort_utils","title":"tablite.sort_utils","text":""},{"location":"reference/sort_utils/#tablite.sort_utils-attributes","title":"Attributes","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.uca_collator","title":"tablite.sort_utils.uca_collator = Collator() module-attribute","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.modes","title":"tablite.sort_utils.modes = {'alphanumeric': text_sort, 'unix': unix_sort, 'excel': excel_sort} module-attribute","text":""},{"location":"reference/sort_utils/#tablite.sort_utils-functions","title":"Functions","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.text_sort","title":"tablite.sort_utils.text_sort(values, reverse=False)","text":"

Sorts everything as text.

Source code in tablite/sort_utils.py
def text_sort(values, reverse=False):\n    \"\"\"\n    Sorts everything as text.\n    \"\"\"\n    text = {str(i): i for i in values}\n    L = list(text.keys())\n    L.sort(key=uca_collator.sort_key, reverse=reverse)\n    d = {text[value]: ix for ix, value in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.unix_sort","title":"tablite.sort_utils.unix_sort(values, reverse=False)","text":"

Unix sortation sorts by the following order:

| rank | type | value | +------+-----------+--------------------------------------------+ | 0 | None | floating point -infinite | | 1 | bool | 0 as False, 1 as True | | 2 | int | as numeric value | | 2 | float | as numeric value | | 3 | time | \u03c4 * seconds into the day / (24 * 60 * 60) | | 4 | date | as integer days since 1970/1/1 | | 5 | datetime | as float using date (int) + time (decimal) | | 6 | timedelta | as float using date (int) + time (decimal) | | 7 | str | using unicode | +------+-----------+--------------------------------------------+

\u03c4 = 2 * \u03c0

Source code in tablite/sort_utils.py
def unix_sort(values, reverse=False):\n    \"\"\"\n    Unix sortation sorts by the following order:\n\n    | rank | type      | value                                      |\n    +------+-----------+--------------------------------------------+\n    |   0  | None      | floating point -infinite                   |\n    |   1  | bool      | 0 as False, 1 as True                      |\n    |   2  | int       | as numeric value                           |\n    |   2  | float     | as numeric value                           |\n    |   3  | time      | \u03c4 * seconds into the day / (24 * 60 * 60)  |\n    |   4  | date      | as integer days since 1970/1/1             |\n    |   5  | datetime  | as float using date (int) + time (decimal) |\n    |   6  | timedelta | as float using date (int) + time (decimal) |\n    |   7  | str       | using unicode                              |\n    +------+-----------+--------------------------------------------+\n\n    \u03c4 = 2 * \u03c0\n\n    \"\"\"\n    text, non_text = [], []\n\n    # L = []\n    # text = [i for i in values if isinstance(i, str)]\n    # text.sort(key=uca_collator.sort_key, reverse=reverse)\n    # text_code = _unix_typecodes[str]\n    # L = [(text_code, ix, v) for ix, v in enumerate(text)]\n\n    for value in values:\n        if isinstance(value, str):\n            text.append(value)\n        else:\n            t = type(value)\n            TC = _unix_typecodes[t]\n            tf = _unix_value_function[t]\n            VC = tf(value)\n            non_text.append((TC, VC, value))\n    non_text.sort(reverse=reverse)\n\n    text.sort(key=uca_collator.sort_key, reverse=reverse)\n    text_code = _unix_typecodes[str]\n    text = [(text_code, ix, v) for ix, v in enumerate(text)]\n\n    L = non_text + text\n    d = {value: ix for ix, (_, _, value) in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.excel_sort","title":"tablite.sort_utils.excel_sort(values, reverse=False)","text":"

Excel sortation sorts by the following order:

| rank | type | value | +------+-----------+--------------------------------------------+ | 1 | int | as numeric value | | 1 | float | as numeric value | | 1 | time | as seconds into the day / (24 * 60 * 60) | | 1 | date | as integer days since 1900/1/1 | | 1 | datetime | as float using date (int) + time (decimal) | | (1)*| timedelta | as float using date (int) + time (decimal) | | 2 | str | using unicode | | 3 | bool | 0 as False, 1 as True | | 4 | None | floating point infinite. | +------+-----------+--------------------------------------------+

  • Excel doesn't have timedelta.
Source code in tablite/sort_utils.py
def excel_sort(values, reverse=False):\n    \"\"\"\n    Excel sortation sorts by the following order:\n\n    | rank | type      | value                                      |\n    +------+-----------+--------------------------------------------+\n    |   1  | int       | as numeric value                           |\n    |   1  | float     | as numeric value                           |\n    |   1  | time      | as seconds into the day / (24 * 60 * 60)   |\n    |   1  | date      | as integer days since 1900/1/1             |\n    |   1  | datetime  | as float using date (int) + time (decimal) |\n    |  (1)*| timedelta | as float using date (int) + time (decimal) |\n    |   2  | str       | using unicode                              |\n    |   3  | bool      | 0 as False, 1 as True                      |\n    |   4  | None      | floating point infinite.                   |\n    +------+-----------+--------------------------------------------+\n\n    * Excel doesn't have timedelta.\n    \"\"\"\n\n    def tup(TC, value):\n        return (TC, _excel_value_function[t](value), value)\n\n    text, numeric, booles, nones = [], [], [], []\n    for value in values:\n        t = type(value)\n        TC = _excel_typecodes[t]\n\n        if TC == 0:\n            numeric.append(tup(TC, value))\n        elif TC == 1:\n            text.append(value)  # text is processed later.\n        elif TC == 2:\n            booles.append(tup(TC, value))\n        elif TC == 3:\n            booles.append(tup(TC, value))\n        else:\n            raise TypeError(f\"no typecode for {value}\")\n\n    if text:\n        text.sort(key=uca_collator.sort_key, reverse=reverse)\n        text = [(2, ix, v) for ix, v in enumerate(text)]\n\n    numeric.sort(reverse=reverse)\n    booles.sort(reverse=reverse)\n    nones.sort(reverse=reverse)\n\n    if reverse:\n        L = nones + booles + text + numeric\n    else:\n        L = numeric + text + booles + nones\n    d = {value: ix for ix, (_, _, value) in enumerate(L)}\n    return d\n
"},{"location":"reference/sort_utils/#tablite.sort_utils.rank","title":"tablite.sort_utils.rank(values, reverse, mode)","text":"

values: list of values to sort. reverse: bool mode: as 'text', as 'numeric' or as 'excel' return: dict: d[value] = rank

Source code in tablite/sort_utils.py
def rank(values, reverse, mode):\n    \"\"\"\n    values: list of values to sort.\n    reverse: bool\n    mode: as 'text', as 'numeric' or as 'excel'\n    return: dict: d[value] = rank\n    \"\"\"\n    if mode not in modes:\n        raise ValueError(f\"{mode} not in list of modes: {list(modes)}\")\n    f = modes.get(mode)\n    return f(values, reverse)\n
"},{"location":"reference/sortation/","title":"Sortation","text":""},{"location":"reference/sortation/#tablite.sortation","title":"tablite.sortation","text":""},{"location":"reference/sortation/#tablite.sortation-attributes","title":"Attributes","text":""},{"location":"reference/sortation/#tablite.sortation-classes","title":"Classes","text":""},{"location":"reference/sortation/#tablite.sortation-functions","title":"Functions","text":""},{"location":"reference/sortation/#tablite.sortation.sort_index","title":"tablite.sortation.sort_index(T, mapping, sort_mode='excel', tqdm=_tqdm, pbar=None)","text":"

helper for methods sort and is_sorted

param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default) param: **kwargs: sort criteria. See Table.sort()

Source code in tablite/sortation.py
def sort_index(T, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar=None):\n    \"\"\"\n    helper for methods `sort` and `is_sorted`\n\n    param: sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" (default)\n    param: **kwargs: sort criteria. See Table.sort()\n    \"\"\"\n\n    sub_cls_check(T, Table)\n\n    if not isinstance(mapping, dict) or not mapping:\n        raise TypeError(\"Expected mapping (dict)?\")\n\n    for k, v in mapping.items():\n        if k not in T.columns:\n            raise ValueError(f\"no column {k}\")\n        if not isinstance(v, bool):\n            raise ValueError(f\"{k} was mapped to {v} - a non-boolean\")\n\n    if sort_mode not in sort_modes:\n        raise ValueError(f\"{sort_mode} not in list of sort_modes: {list(sort_modes)}\")\n\n    rank = {i: tuple() for i in range(len(T))}  # create index and empty tuple for sortation.\n\n    _pbar = tqdm(total=len(mapping.items()), desc=\"creating sort index\") if pbar is None else pbar\n\n    for key, reverse in mapping.items():\n        col = T[key][:]\n        ranks = sort_rank(values=[numpy_to_python(v) for v in multitype_set(col)], reverse=reverse, mode=sort_mode)\n        assert isinstance(ranks, dict)\n        for ix, v in enumerate(col):\n            v2 = numpy_to_python(v)\n            rank[ix] += (ranks[v2],)  # add tuple for each sortation level.\n\n        _pbar.update(1)\n\n    del col\n    del ranks\n\n    new_order = [(r, i) for i, r in rank.items()]  # tuples are listed and sort...\n    del rank  # free memory.\n\n    new_order.sort()\n    sorted_index = [i for _, i in new_order]  # new index is extracted.\n    new_order.clear()\n    return np.array(sorted_index, dtype=np.int64)\n
"},{"location":"reference/sortation/#tablite.sortation.reindex","title":"tablite.sortation.reindex(T, index)","text":"

index: list of integers that declare sort order.

Examples:

Table:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6]\nresult: ['b','d','f','h']\n\nTable:  ['a','b','c','d','e','f','g','h']\nindex:  [0,2,4,6,1,3,5,7]\nresult: ['a','c','e','g','b','d','f','h']\n
Source code in tablite/sortation.py
def reindex(T, index):\n    \"\"\"\n    index: list of integers that declare sort order.\n\n    Examples:\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6]\n        result: ['b','d','f','h']\n\n        Table:  ['a','b','c','d','e','f','g','h']\n        index:  [0,2,4,6,1,3,5,7]\n        result: ['a','c','e','g','b','d','f','h']\n\n    \"\"\"\n    sub_cls_check(T, Table)\n    if isinstance(index, list):\n        index = np.array(index, dtype=int)\n    type_check(index, np.ndarray)\n    if max(index) >= len(T):\n        raise IndexError(\"index out of range: max(index) > len(self)\")\n    if min(index) < -len(T):\n        raise IndexError(\"index out of range: min(index) < -len(self)\")\n\n    fields = len(T) * len(T.columns)\n    m = select_processing_method(fields, _reindex, _mp_reindex)\n    return m(T, index)\n
"},{"location":"reference/sortation/#tablite.sortation.sort","title":"tablite.sortation.sort(T, mapping, sort_mode='excel', tqdm=_tqdm, pbar: _tqdm = None)","text":"

Perform multi-pass sorting with precedence given order of column names. sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\" kwargs: keys: columns, values: 'reverse' as boolean.

examples: Table.sort('A'=False) means sort by 'A' in ascending order. Table.sort('A'=True, 'B'=False) means sort 'A' in descending order, then (2nd priority) sort B in ascending order.

Source code in tablite/sortation.py
def sort(T, mapping, sort_mode=\"excel\", tqdm=_tqdm, pbar: _tqdm = None):\n    \"\"\"Perform multi-pass sorting with precedence given order of column names.\n    sort_mode: str: \"alphanumeric\", \"unix\", or, \"excel\"\n    kwargs:\n        keys: columns,\n        values: 'reverse' as boolean.\n\n    examples:\n    Table.sort('A'=False) means sort by 'A' in ascending order.\n    Table.sort('A'=True, 'B'=False) means sort 'A' in descending order, then (2nd priority)\n    sort B in ascending order.\n    \"\"\"\n    sub_cls_check(T, Table)\n\n    index = sort_index(T, mapping, sort_mode=sort_mode, tqdm=_tqdm, pbar=pbar)\n    m = select_processing_method(len(T) * len(T.columns), _sp_reindex, _mp_reindex)\n    return m(T, index, tqdm=tqdm, pbar=pbar)\n
"},{"location":"reference/sortation/#tablite.sortation.is_sorted","title":"tablite.sortation.is_sorted(T, mapping, sort_mode='excel')","text":"

Performs multi-pass sorting check with precedence given order of column names.

PARAMETER DESCRIPTION mapping

sort criteria. See Table.sort()

RETURNS DESCRIPTION

bool

Source code in tablite/sortation.py
def is_sorted(T, mapping, sort_mode=\"excel\"):\n    \"\"\"Performs multi-pass sorting check with precedence given order of column names.\n\n    Args:\n        mapping: sort criteria. See Table.sort()\n        sort_mode = sort mode. See Table.sort()\n\n    Returns:\n        bool\n    \"\"\"\n    index = sort_index(T, mapping, sort_mode=sort_mode)\n    match = np.arange(len(T))\n    return np.all(index == match)\n
"},{"location":"reference/tools/","title":"Tools","text":""},{"location":"reference/tools/#tablite.tools","title":"tablite.tools","text":""},{"location":"reference/tools/#tablite.tools-attributes","title":"Attributes","text":""},{"location":"reference/tools/#tablite.tools.guess","title":"tablite.tools.guess = DataTypes.guess module-attribute","text":""},{"location":"reference/tools/#tablite.tools.xround","title":"tablite.tools.xround = DataTypes.round module-attribute","text":""},{"location":"reference/tools/#tablite.tools-classes","title":"Classes","text":""},{"location":"reference/tools/#tablite.tools-functions","title":"Functions","text":""},{"location":"reference/tools/#tablite.tools.head","title":"tablite.tools.head(path, linecount=5, delimiter=None)","text":"

Gets the head of any supported file format.

Source code in tablite/tools.py
def head(path, linecount=5, delimiter=None):\n    \"\"\"\n    Gets the head of any supported file format.\n    \"\"\"\n    return get_headers(path, linecount=linecount, delimiter=delimiter)\n
"},{"location":"reference/utils/","title":"Utils","text":""},{"location":"reference/utils/#tablite.utils","title":"tablite.utils","text":""},{"location":"reference/utils/#tablite.utils-attributes","title":"Attributes","text":""},{"location":"reference/utils/#tablite.utils.letters","title":"tablite.utils.letters = string.ascii_lowercase + string.digits module-attribute","text":""},{"location":"reference/utils/#tablite.utils.required_keys","title":"tablite.utils.required_keys = {'min', 'max', 'mean', 'median', 'stdev', 'mode', 'distinct', 'iqr_low', 'iqr_high', 'iqr', 'sum', 'summary type', 'histogram'} module-attribute","text":""},{"location":"reference/utils/#tablite.utils.summary_methods","title":"tablite.utils.summary_methods = {bool: _boolean_statistics_summary, int: _numeric_statistics_summary, float: _numeric_statistics_summary, str: _string_statistics_summary, date: _date_statistics_summary, datetime: _datetime_statistics_summary, time: _time_statistics_summary, timedelta: _timedelta_statistics_summary, type(None): _none_type_summary} module-attribute","text":""},{"location":"reference/utils/#tablite.utils-functions","title":"Functions","text":""},{"location":"reference/utils/#tablite.utils.generate_random_string","title":"tablite.utils.generate_random_string(len)","text":"Source code in tablite/utils.py
def generate_random_string(len):\n    return \"\".join(random.choice(letters) for i in range(len))\n
"},{"location":"reference/utils/#tablite.utils.type_check","title":"tablite.utils.type_check(var, kind)","text":"Source code in tablite/utils.py
def type_check(var, kind):\n    if not isinstance(var, kind):\n        raise TypeError(f\"Expected {kind}, not {type(var)}\")\n
"},{"location":"reference/utils/#tablite.utils.sub_cls_check","title":"tablite.utils.sub_cls_check(c, kind)","text":"Source code in tablite/utils.py
def sub_cls_check(c, kind):\n    if not issubclass(type(c), kind):\n        raise TypeError(f\"Expected {kind}, not {type(c)}\")\n
"},{"location":"reference/utils/#tablite.utils.name_check","title":"tablite.utils.name_check(options, *names)","text":"Source code in tablite/utils.py
def name_check(options, *names):\n    for n in names:\n        if n not in options:\n            raise ValueError(f\"{n} not in {options}\")\n
"},{"location":"reference/utils/#tablite.utils.unique_name","title":"tablite.utils.unique_name(wanted_name, set_of_names)","text":"

returns a wanted_name as wanted_name_i given a list of names which guarantees unique naming.

Source code in tablite/utils.py
def unique_name(wanted_name, set_of_names):\n    \"\"\"\n    returns a wanted_name as wanted_name_i given a list of names\n    which guarantees unique naming.\n    \"\"\"\n    if not isinstance(set_of_names, set):\n        set_of_names = set(set_of_names)\n    name, i = wanted_name, 1\n    while name in set_of_names:\n        name = f\"{wanted_name}_{i}\"\n        i += 1\n    return name\n
"},{"location":"reference/utils/#tablite.utils.expression_interpreter","title":"tablite.utils.expression_interpreter(expression, columns)","text":"

Interprets valid expressions such as:

\"all((A==B, C!=4, 200<D))\"\n
as

def _f(A,B,C,D): return all((A==B, C!=4, 200<D))

using python's compiler.

Source code in tablite/utils.py
def expression_interpreter(expression, columns):\n    \"\"\"\n    Interprets valid expressions such as:\n\n        \"all((A==B, C!=4, 200<D))\"\n\n    as:\n        def _f(A,B,C,D):\n            return all((A==B, C!=4, 200<D))\n\n    using python's compiler.\n    \"\"\"\n    if not isinstance(expression, str):\n        raise TypeError(f\"`{expression}` is not a str\")\n    if not isinstance(columns, list):\n        raise TypeError\n    if not all(isinstance(i, str) for i in columns):\n        raise TypeError\n\n    req_columns = \", \".join(i for i in columns if i in expression)\n    script = f\"def f({req_columns}):\\n    return {expression}\"\n    tree = ast.parse(script)\n    code = compile(tree, filename=\"blah\", mode=\"exec\")\n    namespace = {}\n    exec(code, namespace)\n    f = namespace[\"f\"]\n    if not callable(f):\n        raise ValueError(f\"The expression could not be parse: {expression}\")\n    return f\n
"},{"location":"reference/utils/#tablite.utils.intercept","title":"tablite.utils.intercept(A, B)","text":"

Enables calculation of the intercept of two range objects. Used to determine if a datablock contains a slice.

PARAMETER DESCRIPTION A

range

B

range

RETURNS DESCRIPTION range

The intercept of ranges A and B.

Source code in tablite/utils.py
def intercept(A, B):\n    \"\"\"Enables calculation of the intercept of two range objects.\n    Used to determine if a datablock contains a slice.\n\n    Args:\n        A: range\n        B: range\n\n    Returns:\n        range: The intercept of ranges A and B.\n    \"\"\"\n    type_check(A, range)\n    type_check(B, range)\n\n    if A.step < 1:\n        A = range(A.stop + 1, A.start + 1, 1)\n    if B.step < 1:\n        B = range(B.stop + 1, B.start + 1, 1)\n\n    if len(A) == 0:\n        return range(0)\n    if len(B) == 0:\n        return range(0)\n\n    if A.stop <= B.start:\n        return range(0)\n    if A.start >= B.stop:\n        return range(0)\n\n    if A.start <= B.start:\n        if A.stop <= B.stop:\n            start, end = B.start, A.stop\n        elif A.stop > B.stop:\n            start, end = B.start, B.stop\n        else:\n            raise ValueError(\"bad logic\")\n    elif A.start < B.stop:\n        if A.stop <= B.stop:\n            start, end = A.start, A.stop\n        elif A.stop > B.stop:\n            start, end = A.start, B.stop\n        else:\n            raise ValueError(\"bad logic\")\n    else:\n        raise ValueError(\"bad logic\")\n\n    a_steps = math.ceil((start - A.start) / A.step)\n    a_start = (a_steps * A.step) + A.start\n\n    b_steps = math.ceil((start - B.start) / B.step)\n    b_start = (b_steps * B.step) + B.start\n\n    if A.step == 1 or B.step == 1:\n        start = max(a_start, b_start)\n        step = max(A.step, B.step)\n        return range(start, end, step)\n    elif A.step == B.step:\n        a, b = min(A.start, B.start), max(A.start, B.start)\n        if (b - a) % A.step != 0:  # then the ranges are offset.\n            return range(0)\n        else:\n            return range(b, end, step)\n    else:\n        # determine common step size:\n        step = max(A.step, B.step) if math.gcd(A.step, B.step) != 1 else A.step * B.step\n        # examples:\n        # 119 <-- 17 if 1 != 1 else 119 <-- max(7, 17) if math.gcd(7, 17) != 1 else 7 * 17\n        #  30 <-- 30 if 3 != 1 else 90 <-- max(3, 30) if math.gcd(3, 30) != 1 else 3*30\n        if A.step < B.step:\n            for n in range(a_start, end, A.step):  # increment in smallest step to identify the first common value.\n                if n < b_start:\n                    continue\n                elif (n - b_start) % B.step == 0:\n                    return range(n, end, step)  # common value found.\n        else:\n            for n in range(b_start, end, B.step):\n                if n < a_start:\n                    continue\n                elif (n - a_start) % A.step == 0:\n                    return range(n, end, step)\n\n        return range(0)\n
"},{"location":"reference/utils/#tablite.utils.summary_statistics","title":"tablite.utils.summary_statistics(values, counts)","text":"

values: any type counts: integer

returns dict with: - min (int/float, length of str, date) - max (int/float, length of str, date) - mean (int/float, length of str, date) - median (int/float, length of str, date) - stdev (int/float, length of str, date) - mode (int/float, length of str, date) - distinct (number of distinct values) - iqr (int/float, length of str, date) - sum (int/float, length of str, date) - histogram (2 arrays: values, count of each values)

Source code in tablite/utils.py
def summary_statistics(values, counts):\n    \"\"\"\n    values: any type\n    counts: integer\n\n    returns dict with:\n    - min (int/float, length of str, date)\n    - max (int/float, length of str, date)\n    - mean (int/float, length of str, date)\n    - median (int/float, length of str, date)\n    - stdev (int/float, length of str, date)\n    - mode (int/float, length of str, date)\n    - distinct (number of distinct values)\n    - iqr (int/float, length of str, date)\n    - sum (int/float, length of str, date)\n    - histogram (2 arrays: values, count of each values)\n    \"\"\"\n    # determine the dominant datatype:\n    dtypes = defaultdict(int)\n    most_frequent, most_frequent_dtype = 0, int\n    for v, c in zip(values, counts):\n        dtype = type(v)\n        total = dtypes[dtype] + c\n        dtypes[dtype] = total\n        if total > most_frequent:\n            most_frequent_dtype = dtype\n            most_frequent = total\n\n    if most_frequent == 0:\n        return {}\n\n    most_frequent_dtype = max(dtypes, key=dtypes.get)\n    mask = [type(v) == most_frequent_dtype for v in values]\n    v = list(compress(values, mask))\n    c = list(compress(counts, mask))\n\n    f = summary_methods.get(most_frequent_dtype, int)\n    result = f(v, c)\n    result[\"distinct\"] = len(values)\n    result[\"summary type\"] = most_frequent_dtype.__name__\n    result[\"histogram\"] = [values, counts]\n    assert set(result.keys()) == required_keys, \"Key missing!\"\n    return result\n
"},{"location":"reference/utils/#tablite.utils.date_range","title":"tablite.utils.date_range(start, stop, step)","text":"Source code in tablite/utils.py
def date_range(start, stop, step):\n    if not isinstance(start, datetime):\n        raise TypeError(\"start is not datetime\")\n    if not isinstance(stop, datetime):\n        raise TypeError(\"stop is not datetime\")\n    if not isinstance(step, timedelta):\n        raise TypeError(\"step is not timedelta\")\n    n = (stop - start) // step\n    return [start + step * i for i in range(n)]\n
"},{"location":"reference/utils/#tablite.utils.dict_to_rows","title":"tablite.utils.dict_to_rows(d)","text":"Source code in tablite/utils.py
def dict_to_rows(d):\n    type_check(d, dict)\n    rows = []\n    max_length = max(len(i) for i in d.values())\n    order = list(d.keys())\n    rows.append(order)\n    for i in range(max_length):\n        row = [d[k][i] for k in order]\n        rows.append(row)\n    return rows\n
"},{"location":"reference/utils/#tablite.utils.calc_col_count","title":"tablite.utils.calc_col_count(letters: str)","text":"Source code in tablite/utils.py
def calc_col_count(letters: str):\n    ord_nil = ord(\"A\") - 1\n    cols_per_letter = ord(\"Z\") - ord_nil\n    col_count = 0\n\n    for i, v in enumerate(reversed(letters)):\n        col_count = col_count + (ord(v) - ord_nil) * pow(cols_per_letter, i)\n\n    return col_count\n
"},{"location":"reference/utils/#tablite.utils.calc_true_dims","title":"tablite.utils.calc_true_dims(sheet)","text":"Source code in tablite/utils.py
def calc_true_dims(sheet):\n    src = sheet._get_source()\n    last_column = None\n\n    def handleStartElement(name, attrs):\n        nonlocal last_column\n        if name == \"c\":\n            last_column = attrs\n\n    parser = expat.ParserCreate()\n    parser.buffer_text = True\n    parser.StartElementHandler = handleStartElement\n    parser.ParseFile(src)\n\n    last_index = last_column[\"r\"]\n\n    regex = re.compile(\"\\d+\")\n\n    idx, _ = next(regex.finditer(last_index)).span()\n\n    letters, digits = last_index[0:idx], int(last_index[idx:])\n\n    return calc_col_count(letters), digits\n
"},{"location":"reference/utils/#tablite.utils.fixup_worksheet","title":"tablite.utils.fixup_worksheet(worksheet)","text":"Source code in tablite/utils.py
def fixup_worksheet(worksheet):\n    try:\n        ws_cols, ws_rows = calc_true_dims(worksheet)\n\n        worksheet._max_column = ws_cols\n        worksheet._max_row = ws_rows\n    except Exception as e:\n        logging.error(f\"Failed to fetch true dimensions: {e}\")\n
"},{"location":"reference/utils/#tablite.utils.update_access_time","title":"tablite.utils.update_access_time(path)","text":"Source code in tablite/utils.py
def update_access_time(path):\n    path = Path(path)\n    stat = path.stat()\n    os.utime(path, (now(), stat.st_mtime))\n
"},{"location":"reference/utils/#tablite.utils.load_numpy","title":"tablite.utils.load_numpy(path)","text":"Source code in tablite/utils.py
def load_numpy(path):\n    update_access_time(path)\n\n    return np.load(path, allow_pickle=True, fix_imports=False)\n
"},{"location":"reference/version/","title":"Version","text":""},{"location":"reference/version/#tablite.version","title":"tablite.version","text":""},{"location":"reference/version/#tablite.version-attributes","title":"Attributes","text":""},{"location":"reference/version/#tablite.version.__version_info__","title":"tablite.version.__version_info__ = (major, minor, patch) module-attribute","text":""},{"location":"reference/version/#tablite.version.__version__","title":"tablite.version.__version__ = '.'.join(str(i) for i in __version_info__) module-attribute","text":""}]} \ No newline at end of file diff --git a/master/sitemap.xml.gz b/master/sitemap.xml.gz index 9df622e928f63c5c7c7639771083589b66f5181a..7bb818fd2b0c7172f85be2233460b4a5f23610fd 100644 GIT binary patch delta 15 WcmbQpJdv4AzMF%iYQsjhZbkqhMFds= delta 15 WcmbQpJdv4AzMF%?p>88vHzNQZ?*r-p diff --git a/master/tablite/__pycache__/__init__.cpython-310.pyc b/master/tablite/__pycache__/__init__.cpython-310.pyc index 62b72e23b880bdce4075aae043cc5fee62b2b3c7..6924d9c2d3e1bdb2636609eaca69cb4125013307 100644 GIT binary patch delta 19 ZcmZ3)yoi}ApO=@50SIh2Y~-532mmOJ1StRj delta 19 ZcmZ3)yoi}ApO=@50SGwiHge5i1OO&g1Hu3R diff --git a/master/tablite/__pycache__/base.cpython-310.pyc b/master/tablite/__pycache__/base.cpython-310.pyc index bab3fc29efdc896f673c9b2e007a26dd6bd58d8d..0c21d41dbf13a0f68977199208449c3b68780737 100644 GIT binary patch delta 21 bcmdmUf_cvgX0Ci*UM>b8u-&kc%jgLJOKk>f delta 21 bcmdmUf_cvgX0Ci*UM>b8;HcZkW%L99Ni_xo diff --git a/master/tablite/__pycache__/config.cpython-310.pyc b/master/tablite/__pycache__/config.cpython-310.pyc index 4f10c486b306c01b3781efe221a08eec07a58752..122818fec43abaf185019c136be954b528b1fd5d 100644 GIT binary patch delta 19 ZcmZ1^x=55OpO=@50SIh2Y~-531pq4g1Xch5 delta 19 ZcmZ1^x=55OpO=@50SGwiHge720stt&1MdI; diff --git a/master/tablite/__pycache__/core.cpython-310.pyc b/master/tablite/__pycache__/core.cpython-310.pyc index cd241395869ded83024d9ebc56c980f060631649..4a77ccb869dea0698070cc1585e103fffea17b98 100644 GIT binary patch delta 21 bcmZqq%-Hgokt?5b8u-&kcOREw9Plg6J delta 21 bcmdn;iE+y(My`BbUM>b8;HcZkrBw+4O-=>S diff --git a/master/tablite/__pycache__/diff.cpython-310.pyc b/master/tablite/__pycache__/diff.cpython-310.pyc index 4c5f99b256d9cd00f2fffe136be8da504b772300..250409de27c754377ba69bf9619265232698da9f 100644 GIT binary patch delta 19 ZcmaDU`cjlDpO=@50SIh2Y~;Gn1pqaK1uFmm delta 19 ZcmaDU`cjlDpO=@50SGwiHgetP0su2i1jGOU diff --git a/master/tablite/__pycache__/export_utils.cpython-310.pyc b/master/tablite/__pycache__/export_utils.cpython-310.pyc index a9e33ee4e1776af9414aa580128754c00c63613b..2d169f2905281357e39c8eede7296a92404cfd89 100644 GIT binary patch delta 19 ZcmccMbis)$pO=@50SIh2Y~*rN1OPVQ1knHh delta 19 ZcmccMbis)$pO=@50SGwiHgdTs0su6p1Zn^P diff --git a/master/tablite/__pycache__/file_reader_utils.cpython-310.pyc b/master/tablite/__pycache__/file_reader_utils.cpython-310.pyc index ddf3a4aa02b14f6e998744cecfd7deff7744bfcf..91622af9aa28b1b201a35aed2dac21d187e8eb78 100644 GIT binary patch delta 49 zcmbPgH`R_SpO=@50SIh2Y~)(RC-{r8<`*MpkpxiW7o*iLM(fRc`FhzI{Wp7yo#Frh DP{0{}Zh1(yH- delta 19 ZcmaEF`QDN%pO=@50SGwiHgY|c0RTA)1uy^r diff --git a/master/tablite/__pycache__/groupbys.cpython-310.pyc b/master/tablite/__pycache__/groupbys.cpython-310.pyc index d087a1d13e7937f598ed2d7332a8e373373bbc7e..8753ae72a3ed4f23e9e7ded273dc97bba218ff77 100644 GIT binary patch delta 19 ZcmZ3kzg(XypO=@50SIh2Y~-3N4gfDX1f~E0 delta 19 ZcmZ3kzg(XypO=@50SGwiHge4s2LLSr1U~=( diff --git a/master/tablite/__pycache__/import_utils.cpython-310.pyc b/master/tablite/__pycache__/import_utils.cpython-310.pyc index 83c10d9786c840ab9c67c701b956c2dc6ea6294f..ce5f07a31ada89462a8b48f64170d7f100badd31 100644 GIT binary patch delta 21 bcmey-%lM<0kt?5kNqN@fNh diff --git a/master/tablite/__pycache__/imputation.cpython-310.pyc b/master/tablite/__pycache__/imputation.cpython-310.pyc index e8690b51533e0641d706b9a238dc9283e2910fd7..3798fd993f4f2c23416aa3750983ff442a0dc505 100644 GIT binary patch delta 31 kcmca=e$|{SpO=@50SIh2Y~)hnV&eufi?}u$aV-`E0Co)qx&QzG delta 31 jcmca=e$|{SpO=@50SGwiHgYL(v4Pk{+?$QK77GFZZu|yQ diff --git a/master/tablite/__pycache__/joins.cpython-310.pyc b/master/tablite/__pycache__/joins.cpython-310.pyc index 4f890910f50a3094ed607ff1206b4a596c5aa380..f4cf2db9a7caf298594ece99b08185839282137d 100644 GIT binary patch delta 19 ZcmaDF`81L%pO=@50SIh2Y~;GF0{}i21<3#a delta 19 ZcmaDF`81L%pO=@50SGwiHget80RTJR1!4dI diff --git a/master/tablite/__pycache__/lookup.cpython-310.pyc b/master/tablite/__pycache__/lookup.cpython-310.pyc index 03ecc48f8e9f2e07a6499445f420a0795e092765..8f09feb019d3d026b74443adacbbf25a0a95da4f 100644 GIT binary patch delta 19 ZcmX@9a8iLQpO=@50SIh2Y~->R001#S1ZMyM delta 19 ZcmX@9a8iLQpO=@50SGwiHgeeu001ut1ONa4 diff --git a/master/tablite/__pycache__/match.cpython-310.pyc b/master/tablite/__pycache__/match.cpython-310.pyc index 3fdcae7bb38ed09326d7418136e6b8b82c34d3dd..0e9914995eced956a5785502598f0f08be58d44f 100644 GIT binary patch delta 19 YcmZn_Z58Fp=jG*M00P?$8@Z~v04A&h{{R30 delta 19 YcmZn_Z58Fp=jG*M00NG>ja=1S03>t+m;e9( diff --git a/master/tablite/__pycache__/merge.cpython-310.pyc b/master/tablite/__pycache__/merge.cpython-310.pyc index f18ba73e24be2737432a36325f1f6252f7f23266..3ac8b46fe989b52d6fb18e9eab75bda2811cea4a 100644 GIT binary patch delta 19 ZcmaFO`I?g}pO=@50SIh2Y~*^#0su5J1rq=O delta 19 ZcmaFO`I?g}pO=@50SGwiHgY{=0RS=j1gro6 diff --git a/master/tablite/__pycache__/mp_utils.cpython-310.pyc b/master/tablite/__pycache__/mp_utils.cpython-310.pyc index e9ebe55f8e33bbd9a873f6b602e3b3548fc8b083..134cc1a680d58edc85322c0d4ec31a3bac6f10c0 100644 GIT binary patch delta 19 ZcmcaEc3q4spO=@50SIh2Y~=Fg1^_Yp1b6@d delta 19 ZcmcaEc3q4spO=@50SGwiHgfrL0{}0>1Q7rL diff --git a/master/tablite/__pycache__/nimlite.cpython-310.pyc b/master/tablite/__pycache__/nimlite.cpython-310.pyc index a042aa87fff8a74f5b6fd23c365f8d5b2871c757..83d8e5ba0ab6d448cca641d075c36b8177de2f8c 100644 GIT binary patch delta 51 zcmX?Od)k&apO=@50SIh2Y)<{Pk@vT-xLS2eYH@O{l|pqvYEg1(UP)qls+FGMW(AQo FTmZk25qbas delta 52 zcmX?Yd&ZVGpO=@50SGwiHmCmH$opGZLcKaAwK%!fN};+SwJ13?uOu-&)yi1UV6(i) G8ZH3Ac@akd diff --git a/master/tablite/__pycache__/pivots.cpython-310.pyc b/master/tablite/__pycache__/pivots.cpython-310.pyc index 9131567aa9689170308f180a65f06c568272d82d..085bb67eee534a65a709f87325e3bcdfdcc4979d 100644 GIT binary patch delta 19 Zcmexs_1B6kpO=@50SIh2Y~(7I1pqvG1w;S< delta 19 Zcmexs_1B6kpO=@50SGwiHgXlq0suNe1l<4t diff --git a/master/tablite/__pycache__/redux.cpython-310.pyc b/master/tablite/__pycache__/redux.cpython-310.pyc index fc0b1f2f21e8ce55e60c66f25a9d9310ed2f4677..278a983fcb5683aa874be0dcd41153d413b690fc 100644 GIT binary patch delta 152 zcmaFn`plIppO=@50SIh2Y~-rpR$@=i&neB#GrGl*T$EXoT9lc13o2xIi>)j%r!;l) z3T`V&6`&5kDuKLwh2qlW3fM zsb#lVa#J^_3S4LA4go4D3Iq|so83ibv6w}I*g+s70z`m}C<+4+0U#n2M1%tgO_rhr zAa#qSIKQZ*D15TGqz+@yWEV+i#(>Ekl2(inn-5D~WD*7IglmEr8M%3-3=1NeajFFRFB%K+9CwE9%F$QivEP0Vh6l?}qCrA^-poq;YWmuTG!A61Q zLpJY_-_6JvGPy*tSpaMy*byMF6}eAlSCU0htFI)@q=&8wq!jGvLLkeI8N@|4zd~gW F9{^KqTjBr! diff --git a/master/tablite/__pycache__/version.cpython-310.pyc b/master/tablite/__pycache__/version.cpython-310.pyc index 0bd8a2ea5f756568b5a0b2e90be880064a726c0a..dc4de48990d88810f256379e294bb24fd8aa6c8a 100644 GIT binary patch delta 24 ecmey%^p}Y%pO=@50SIh2Oyr7ZWZIY|%m@Ha3= szData, "Invalid mask size" let alreadyCast = isCorrectType[desiredName] + let (workdir, pid) = resPass[desiredName] + let pagedir = Path(workdir) / Path("pages") + let dstPath = pagedir / Path($pid & ".npy") originalData.putPage(pageInfosFail, originalName, originalPath.toColSliceInfo) @@ -87,7 +91,7 @@ proc doSliceConvert*(dirPid: Path, pageSize: int, columns: Table[string, string] validMask[i] = VALID - castPathsPass[desiredName] = (originalPath, originalPath, false) + castPathsPass[desiredName] = (originalPath, originalPath, dstPath, false) originalData.putPage(pageInfosPass, desiredName, originalPath.toColSliceInfo) continue @@ -95,14 +99,10 @@ proc doSliceConvert*(dirPid: Path, pageSize: int, columns: Table[string, string] var pathExists = true while pathExists: - castPath = workdir / Path(generateRandomString(5) & ".npy") + castPath = Path(workdir) / Path(generateRandomString(5) & ".npy") pathExists = fileExists(string castPath) - let (workdir, pid) = resPass[desiredName] - let pagedir = Path(workdir) / Path("pages") - let dstPath = pagedir / Path($pid & ".npy") - - castPathsPass[desiredName] = (castPath, dstPath, true) + castPathsPass[desiredName] = (castPath, dstPath, dstPath, true) let desiredType = desiredInfo.`type` let allowEmpty = desiredInfo.allowEmpty @@ -154,13 +154,13 @@ proc doSliceConvert*(dirPid: Path, pageSize: int, columns: Table[string, string] let pathpid = string (Path(dirpid) / Path("pages") / Path($pid & ".npy")) let page = newNDArray(reasonLst) - page.save(pathpid) page.putPage(pageInfosFail, rejectReasonName, resFail[rejectReasonName]) + page.save(pathpid) pagesFail.add((rejectReasonName, pageInfosFail[rejectReasonName])) finally: - for (castPath, _, isTmp) in castPathsPass.values: + for (castPath, _, _, isTmp) in castPathsPass.values: if not isTmp: continue discard tryRemoveFile(string castPath) diff --git a/master/tablite/_nimlite/nimlite.so b/master/tablite/_nimlite/nimlite.so index 5610cd20bddaf36defd9becbd6c8b424f1350475..c56ae19563540ba704100fe11947e58b701ccddb 100644 GIT binary patch delta 432424 zcmafc30%$D`~N*BOShz5X}wfrFGTk2H;PDP&z@xzLe?TnX`*rQgkhSok3od8Ut{01 zH_VVRxG`wPI)?f`=X1{G)A@eCzrWXOIz8`aIp;iQf6nDNXG{8CPRY)q0f>*FAGk__}|~r-ii@SQArMNewQ z2Ck4SDJ&3ps)VNsJX6BI7I>C~eYSu4nc{j!+Xo0nqNJb`c&dbJ1fC_~ zB^9}=w@P?{!1K*GufIlm^(#@MsCwa6Cy9D=~^HaMvYDc!9uEB|JyqSrVQt@U1c~M#otN zNB^69qn#DcxKL8i2;3y$C0Z{3Uc!q6Zdq2|)dd1~RpYGvje_B?W+0v;@L&ng7Pwx* zGX)+k;mHDzmGF4PmHw9~F=B*5s)QF=@)~DJxK7A#mGB6G=S#TGiQ73_=D_>Exd{RU zqfk-^5x7ahBLx0l!s7*QsVTq2XA9i*GtTqZ7zM-sGlLC2oEZfkEa4>r*Gss@nb#~@ z!tDeeE8)5%!AO)C0Rm5z@DPD#NqCIFw@P@j!1ELaKzv2s}%|lLfw2!gX%kPQHX28P59OS&2~~6bdE0NZ=+3*SPZx-b;9lz%6O{ zCP?6IbJB?b^VAEo-J^_gy#r6TEdM2j}`dm{eMgYUgJbbAzt9A5}qvZtrD(@;&$>S zyr{46ba^~U6Co4|%QG})1(SrA^x=lyOSoCyvO;-RXGU^)R|(HnaO{8n5+g?_1WUL% z1HFWoh?+%9xTYV^I9A4^IiARH*8e{bKfy?q6tV@LCE+;&-zwoof#*wjfxyqIaAo}I z`tuSMsu*ni6$#uV;hF(l{=J0j1a4Wed=rES+*QIe2a5f_zr-*O{}^wn}(_!1Eu)_%VUMm+-1$ zd#TIL}{mTQKZCGx+#33EWk}UkF?$;jabmFX0s?@r(l`+$u>Z1WSx6 zLLo%LbpqE*_+P>k5fZK+#50bOaI;5~VkL&zrSTG8Nfb0u!kq-3Ea5c;o+{yi0?*|5 zm;HZx!N`&n!Udi!;ll;KRl*Ymo+IJu0?#jpWBdOrTxTrD&}dc(g|iZ#E${*f-!AY% z2`>=#izIwL>+G!mnIwj}ElVWa+^gS9c(xcd8g2RBo-1%m2|p}wJH(a#ZytW91jAKQ zC=|F(!XF7dM8c;FTrc5s1Rn7P=i^Ut1$rX-3qu)yON2tKgcpfXkSO5+;zA%*!ea!U zCE>}F#r}V*#K;y3`4V0raHE84+_>utB-~hy;~`e%o5n03kt8w93PuSJF=rs*X8927 z|B-=&o8^rH|GfYIoWcK42&qy&1G9XDgn!OJ!p-spm~F0XMc?;*`Hj=M^Dh_Itm;mY`nU(XdnR1Ag~GdUhD;SpvJ zNqF`mE}tynAsHOclJNL!J3juiB}VpYu3(h#B2lwK2~ReANWvo)b2}QR@*XNm<+!Vp zwExdGyGT+fS;!SaBs}{Yjz>$l(d;1!4-s~f%{Z^WMw7gp8_F^>IH@TThO#AGvz7}O zB|JHu{4qnc}K8M2)ldk8o!f?Klys8AvB@;jWI6@SKWVK3T$(N8k-J{>zf^0?PM**)l$y z^7Y>+F?6xqP@#mUT5v-}5}rJY%WGW9_o3|39CwxQOkqdoBJKZ6grN{wp_7GDvuFv= zHeX0c_*Q`%CA{D)w^R5T7voRoDr)wbq2#BDKf@JVUCY-@=gH-xB|JdjSrQ&1aHE84 zl4>dsEHs4@BSI(?Nx0F2%WJBaFHlhpj=M^DjIg7V@FGtxpA;f7bRvUj36B?sVkA67 z$R|st1~|3`=t6-o-3LO~;TK<446@fJ0f@N9F9B|JhD)VvYlcCh{9 zy||%hNg>BvV;Ohn@>vpY6!Jz1Pj1EK3ne_UCiWrL|B57rU9hN`d-*m_WeWH&TEeph zo+aT%fg2?}yM-ta;!6KFyR=9sxO$Y&_`SeGBs{w%Hxw=5c0RnIF%qs3^2uLtKK_aX zBkK!8si{sF%9ijFQ3Iod+X*{`5*{GzXgteT%-AAHxLRU_2nC&l7m19cB|JvRXGwUn zzzZcj%Uqx&uks}d5S_(U!nX>AXbI03c(Q~S2|HO5epbk53;gr`KfvrdNuj`8Qt$Ew zjoB|eC*hWLc-!kFT(_6Yhm^;${e_|E@(hiuFcc%F#l zkNFo$j9{Ul(Uq@piOASh!u3KvM8b1~e6)l|3;AS*v;J=$egR5}aQ&7PV$C&>@BooP zp@b)zD^R0+1>(&WP~pn>OEuR(#bD#lXl@z_&ob9Q!gI_OknpWSK3l>Iaw{nLYm5>j z;(k?@zot;ajp9P0NW!!4ae3F8J$i=8A*qKd(t%eh7sUjK`N=Bx_usKSd>c!&x&sqj`R z+|`cdhV`$E3Exn2u1;ZaTpu+hS#PMJi&fIPO7ox%|fA&9e^(uyW zPGCwADqL%}f@!o0w^HFTD%@Iy$Exru0$1{nS21i<3W+M*R)r_4aP>$?RpI>Y6IaSq z;Z7<$S$1Oo&zitl#mH7ER8!$wRk(`^&r#v7Dm-6>S6AW2&p6iKO~p9-nW4d#z+e6q zsPOh*U`?S4_fX+QD%?|rn^d?L!_DnqqGEWf6yB?FoeI~8Xc_Zd4Ha&w!fUEK1VbdulA@_^qZi9RLKMI2NY+ zudJT#hGpF+(^mBj7GaZVTpPpmur9QAH>>c8u+zBY4<9MUO#fh zD%TXI{aJ;1|0ScyYs2yWGwAO&hQ9_m86pM@Ar^*R1F*^g2uz}}wG?VXZ-x3^uTVP@ z6za}6g?hY7p&T;|-G{`|8aoX8hD=7U_z#^zf4OK_H#CvnziFs5bTWPTr$QCKVU)#h zRkVinL#JCjbk-W`4xMCirM^Pbn?Z&L!za-JJq+zfCeoOG3Uw(?p{^$y4vm~ZM=dqf z85Kc0t})CSHJo~9819b>x2m;Xs|kEwTw|fuFek~|P%xku?Qs~DoeYOZ_pvy820f!c$etu^{H3w+-j za=N(~&d;=lcG@b=P>^fmZpfJIOuH^ulow-x>T8!pTF!l!UP~V5{L?!f<-yo}t6kIEz>JtPQ*8IvOU-&{LzyU_ULwYSeRU zP2k(&ACFn1OU7H&DzHW`f2HkGfbjETn=&Ruo*1&H$7r3LsxZ;1?+x0-iPX8WVMbz{ z_ON3WrNl*sNz+Er%@qwfbDfxd>t8*pXhz$ea&#F?+m) zcTN?<{kV=`Tf?Ru)$GnmhQxV;End&E`E1r=eX2BW4H~r$MfRxXKXa> z53@sFas6pk&A--|X4sO_!09{OzGPH%V@Zqm3C{ZTiha`!rW6m$^^6hpbiJX<0uPdI zsI$PU4nmr#^`i7CZEB|Dullq{303szZRTb;XiTBU%F0Y5%gV}5SIk(S8^7R99rAbX z=$&&YDcSHx4ka&gAMHuAAWsc(`*)CM2G6{9WQbvKUV!COTg=CK`%}ZpJV17X{CrYu zc%1J|`Wh-7=#0OI9jK1IdGrA<5@lF;;5!nP>vwQ7aejeD*;TAF?{ z6dh_szBhOr{zM)b+>R{ApF576CJ}~&qi67E>tm-$&s+J;e0s&QFxY#?;&}@x=0OjL7IjEm5jJXLP`1bXtHuJ;68lbO>cCwHYVH zf|dn2<8Ng_mGP&Eekv=|r=1E-yD?gy(WXjwL${MxNjHPXsRHs_?(WEHPk4PrEEyA}8v1mbk?i+$=O|3D8cGeY==$_ImiqLm zc4a|Lkx>>@3xArv`BzeCjAgs;4A2^)V?6 zD)m=#sf+~XX<5h65C3mf`*lS2rZefUmk$zQ*H*)FqI8KhTLCnk=BMK zclwd-hTreBBja;x-t9t3VD8x8YFOC1IcUNcp3|o{J&R+h`bop;qPnD^;bf7?wmKGR z;a%huAm`oBhC>hPkp_k*4~{rBw9}YQvWzlj*^$zR-_57-~dak zNE%C>tvTJznrxyL4)B*Xsn1gHDo9g@ywg>PAECqnp4*V`sKx=7RwelLvO`{3Rf6>> zgD!UDTT%)S?Z|XY2HKPRS40WiL}Awxf2fz;|Gs7fZ*uZ}W?_Pv2~oKakzG(^6u8}Y=X znH$+lUc)mta!C7%m!Zrb?3)m0IO$I6l2`E3ourVzVTuRohRIP6G6s`cp6GzTp_?a` z<8PSEsJ~#9C+Uw#i6{E`FKFb2>VLriFS4AJz;iFs0h0i4(h-vcZ_*T#ZQdk^HGPpc zadxs`9r<}erMI0cha`lvYKOlBt&X^pm*A%(0hsjFk(!vy&=DObYjk9EQi`o6Xxh%(x|RsidSzDHRg3jv=6Orlak=c6pthCmHP!ZM-hQ5IuEpq>I$NbpJdWRuSV zen(ljQE%I+1etY_iE~2}me_WBZyHouR+jcM@U7{g5Bmj@YGC!AgF{X1aL-{#O)`-D z2?uLpOZ*8|K4cSl201=h^=I(PhlF6#)E9fnQ~1G`^ugqvFX@L#Pd^eviXq33G{EGZ zA2~xzu(=i)hKXfu>_{f)Qk#53okKwdGNg|K)y41lYC68ca zU9yV&k@s(1v_>96o%*CN`8_YWKIu)!1GwIRjKHLMLy}I4;6y_#ToF`iL^@*<)(Csq zefXvk@o~rAz$O-)9PgJEEHK(GSdNo3YnJ=>;YuSiLVKTgmnlxXyF>(Fd*6py0mKcH zRRLJPd+;oPboIC=1RdEr@Is$<#uR&vPi&U=phqBj;5XPDh&|~yxEx5b$geP_F?OgD zNN-H4BeD2@?_!TBJ!3^%gaRfc?@(kA5ea>L{6`n!kyWrcD%p-ST zb5qRV4qR?Z&XZqYbr7lV@e5Zgs`^>&<|VGS?iY9vM8>0Fcry}XH_=uTsZYDEPx%uU z%La%ZX@+I^3JROyq)-SQnv-yH8#0^YG;llbO>?3p)&I%0AG?smlN&cn`5 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

J!kYLACXg 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 I&oM9ge+l3*VE_OC delta 433046 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%0D0-qeOo84{C+@Xr2E06jl4Rq4 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 diff --git a/master/tablite/nimlite.py b/master/tablite/nimlite.py index f431caac..1024f215 100644 --- a/master/tablite/nimlite.py +++ b/master/tablite/nimlite.py @@ -164,7 +164,7 @@ def collect_cs_info(i: int, columns: dict, res_cols_pass: list, res_cols_fail: l def column_select(table, cols, tqdm=_tqdm, TaskManager=TaskManager): - with tqdm(total=100, desc="column select", bar_format='{desc}: {percentage:3.0f}%|{bar}{r_bar}') as pbar: + with tqdm(total=100, desc="column select", bar_format='{desc}: {percentage:.1f}%|{bar}{r_bar}') as pbar: T = type(table) dir_pid = Config.workdir / Config.pid diff --git a/master/tablite/version.py b/master/tablite/version.py index b4edb8ef..e95b6b80 100644 --- a/master/tablite/version.py +++ b/master/tablite/version.py @@ -1,3 +1,3 @@ -major, minor, patch = 2023, 9, 1 +major, minor, patch = 2023, 9, 2 __version_info__ = (major, minor, patch) __version__ = ".".join(str(i) for i in __version_info__)