From 1a4c535dfb25d458657b56ea333f0d0d8b828f79 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Apr 2024 10:17:17 +0000 Subject: [PATCH] Deployed 4d58d36 to master with MkDocs 1.5.3 and mike 2.0.0 --- master/nimlite/funcs/filter.nim | 1 + master/nimlite/libnimlite.so | Bin 1459952 -> 1464104 bytes master/nimlite/numpy.nim | 79 +++- master/objects.inv | Bin 3816 -> 3828 bytes master/reference/base/index.html | 4 +- master/reference/config/index.html | 4 +- master/reference/core/index.html | 4 +- master/reference/datasets/index.html | 4 +- master/reference/datatypes/index.html | 4 +- master/reference/diff/index.html | 4 +- master/reference/export_utils/index.html | 4 +- master/reference/file_reader_utils/index.html | 4 +- master/reference/groupby_utils/index.html | 4 +- master/reference/import_utils/index.html | 4 +- master/reference/imputation/index.html | 4 +- master/reference/joins/index.html | 4 +- master/reference/lookup/index.html | 4 +- master/reference/match/index.html | 4 +- master/reference/merge/index.html | 4 +- master/reference/mp_utils/index.html | 4 +- master/reference/nimlite/index.html | 4 +- master/reference/pivots/index.html | 4 +- master/reference/redux/index.html | 405 +++++++++++++++--- master/reference/reindex/index.html | 4 +- master/reference/sort_utils/index.html | 4 +- master/reference/sortation/index.html | 4 +- master/reference/tools/index.html | 4 +- master/reference/utils/index.html | 4 +- master/reference/version/index.html | 4 +- master/search/search_index.json | 2 +- master/sitemap.xml | 58 +-- master/sitemap.xml.gz | Bin 395 -> 394 bytes master/tablite/redux.py | 181 +++++++- master/tablite/version.py | 2 +- 34 files changed, 655 insertions(+), 169 deletions(-) diff --git a/master/nimlite/funcs/filter.nim b/master/nimlite/funcs/filter.nim index 64f27cb4..b3a9d5a1 100644 --- a/master/nimlite/funcs/filter.nim +++ b/master/nimlite/funcs/filter.nim @@ -147,6 +147,7 @@ proc filter*(table: nimpy.PyObject, pyExpressions: seq[nimpy.PyObject], filterTy let pyType = builtins.getTypeName(pyVal) let obj: PY_ObjectND = ( case pyType + of "NoneType": PY_None of "int": newPY_Object(pyVal.to(int)) of "float": newPY_Object(pyVal.to(float)) of "bool": newPY_Object(pyVal.to(bool)) diff --git a/master/nimlite/libnimlite.so b/master/nimlite/libnimlite.so index 1a3424d3b0284f0feb41af89f0d2285b35d070c3..2fbb9254dae0d97910ef780fa6ed6579b93ed8bf 100644 GIT binary patch delta 569804 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'DzQauInwA<2S~@7_Cq!J3+KW`90* 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 z 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 delta 566245 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
    ==O)M_7lJAAGIvSL?kyT8UCC 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/_*3Gh;3txSwttq7BgmM5q%My${@3d5#0d$jWK2slM(C47_*4&h}D;vg+&}j ztO8@qB2FV#m@#G%*SaB#@MMr##6uMP+J@ACS;Wh(fIVZ3S;QyAt}-TBL?kqm4T~ss zxVN%w!^-|irOwrD$20PBBrHEU72_HER zyV{0DeBy*om|!#<07t+qqQh8Pig?;QOZ4soOA-0Zgkb<4aVNiLDx#N55gxOhEpBO3 zmRLAOE#d>xS^=0-GGR#Q1eI2*Fk!g9MX9TM(?;B%t~B*I7f=2!9HoP{`0)G>A zxYHK8tWE`5n{en!)mlEwPJseBVJarT!ES>s3X}*Z0N~ohTcQqAI!f#JpgpZkq>RGO zvQmfIXex~Q*mzs($J8OM5xCfJbVkt6oWhsoVnNQlKyeu5>=r2A zaQ;Cya&u9x6LE^uZJ=o_Agx>C(=CR@ow{ePKEq~BsPQHiDcNGT!{_g@m3 zh`Md3lpk*rnK;^ovM!d)9%DZP+H`9xw}YTh?2&Gqc3!6W$^|5Vmx6!Oq8gINU^1}* zu@J_XOzc6-i!mk>#}ND0fSxszi3OX|8DlbGXo*V_0~us8Q5^-_F~($~0lqkvF(wnO5i8FalZlh9B(b?E6s{P|U2zM6$;1f6@-oI`Vl`sfaU{oNVmo3l7&DWJ!wBAFkjcd9UjR#E zjLF0`#I`ZUWWoWTT`VyRnaGLQRK}Q0_#*ZjV@xJOn&xN#?1Vuk6A>s_pD`vARq(|X z7-KRKi&$aCB$=2pMXW!p3{WyN$dyKS6~RZ8MCDamYV(1HQy| z&4^7}5)ulw5pRQ@e3fJOSFwD}$=IPbl;Joh6vqUkVG=k1rVOF5Gy)bsu~RJ{ zK*XfNCdx;gQ2)|Nx2jTyHndM6`0{3W!0bs_J4m`N z_8e21=Pd-;{kg3;grCZD@?O;54mxZ+oPY91Ge**H1=D=eb_ISr-sAuzCw1FH zGMoY%C&s10XYfVwC()Bl^`Kw8i;DGWaByjzDmjE>fSNbKw*|FD>^N)x9DV?v_XT)f zo7I6g%}FlEMs6<3efpWEoo2aDW1+shb+%>h|54^ z9NEc@8(>>7RwDXzEody+rOO~GKDoPZY*KC9hRUyD!`AlF)`nz>j+w5-{7y8Z;(Zun zb<4f(WB1l}6UP+SK<^}$I3Oj+C6~G{=A4AD{?8zTFzTDie+)yOVeYTBvXy{Ep_h>- z6VVw5lTLYQ3NSGw$)u5 zvK^67%R8|y@WlLTA~ap8<@Ba7@1TqohtlEj*}-Eo>D+eMKf%khVf(~av^xdoU%VNU ziD%;Bz1ecd&Y6tx&Yda*}5r)(7A;0r!$xiZ%fx}#V8;)Sf^&_nv?}y=+ z93O#-MPKs|gAK}`uF^5Ut`?4f?~UnY-AA%l%3`1>iRCrumvYY4zhy zZK(XPD&%eMgDye7&k*av*jd8fBi4|yy@X{WR*A8d-)Ny+#lu&MGRTMh_@H1O#`v(G z!ifE-OfT+184f_~xx_4o{ftKJ24j5K&s4;YGiE*{uNKBfY-NxS`$<5-MU3%bKgpog z+7!n4upfcg;L0NYEPS@7O&2}RDiO+$hSa>vhKuEAl>)j#L&f2H9}{^lDs`f-9kUxie;oaoZLtdq7$m4)c7$F1 z`d5Rj!Xm}Ii%Nxjvt`d17RsJE1(nkJmlmDhDS6#nmxe|*8frv~ZxEWp5%XcpM3{z{6Jys2 z`v9fVUdtKuNz*aH98m5~dGXIBB{Z--ogjxmKVh@17*pbUo^O^)O30| zI8*eg2`BU1)2ZGG+Xlm<$xg9OVEDoK_wXFxau^yW)>zBM>r{-_kddVwJafa*1hpOV znj&Zv`(dv8P_ZCLCxEIKir6Z~6vE0QHjA+=Ds~KFc=9_w3wl!y+bbTyo(!_$w5kGF zQ^r_vx*=AroJhE$6zi~7=i&edZ%OCBZ-lJBQ)yh7x&!kI_WzJ~I6m{dEV&D_t4JXE z_oYxi^{4qrc4d%1SXMY+gn;?+X%w=hN6jx-D8YzD4L1b8OHdAqD6@9Va$9((b|dtu3+$Q zn!@iy%*fbZgxyDMq{L2|rW5uYvF?lwBPGp%ij| zn->(wX!sH-hTKq!=n7X9i*LY~JXTg5f#if}l+7qjW3mG&V;o{97^_EEbHuhY7D-rV z#Fj8th_HT$O=AorZ$Pg`Rsd`WV_!(Q6A|lFT6DOnMCM&^z^=O#^ z6V(D$A2(AIP$8}4uR@ohOj4#oXdiA>W{(xKGL%A2iwAy`2aoAq|B6xAGt{sc(!?{p zGDYB@Zb7;mJ;j1sihrXA)hHpM7pkbuMCk`7^uPq8;b?seqwdz2CQ5hn;zVgl$S1s> zDe=w_PwjcAaK5cf$g}P)O^gx_QL%i+3UOPRq7JW0FZ1b{DN>I@DMEUPdUup?-KySV z^c^KAd_oc1p0tEIMVo+4Q&98+MVbTPo%+Y_VtSl=vGBZ{Yt0@|Qf9gPPQl;SuxJgZ zCvyGpuQ4L%E=-hKJ_de17$$bS#M`nAbVnBto+wR z1K6lv&=U|i3oX{TJLC-%Q`=WVhKig7-ncqsHslRE z#7VETuw8bOEN%NoajXPHpmnWG898+mmG3JBYs`mPn&~`fB2vdPoOrpboenhSg#DOc zG~}*p(Sg17#e(~aU!?_E&R|$+R`sMWO6i9dl6`2ks?L8y14Lj09CS5zUc}$xb-Wr< z5EnIV`svOUb3Yv$DF*sO98#(WoGI4x6m(XX>awjym!eB9s^d?ZOr?V<*UBhaV&(&- zZo<@HTOGps`B$RKH|Sz*#baqO4RSzZ1Jk!&P;9U6*awGY`^Qd7_tL^+poyYD0<*YX zk+M)A3$u=m!z?b!(F_Od!5}D;rPa&T0!!(V){W~7&wi{0%?9VOHO2`o{3kXECMnp< z0Xa}MxEWQj&h69?pGYo7kIvSb+=g)Md+R+ys(+25 zGM2DOOgWyJU48!seCsL59RD?XR7Q4V)Hg#0U^F~EM>DL|0ixa`rBt+jLsoVlhuUS% z)Gh-X4y`@(Kn?pil*ul01p*;Up2q)st2s3MnB5Uh_)EG9B}XOf1eMbk^%T1wDIvOA zUB&H3N@3l_E+YGp;@f0iM_B2vic>O}Z1$?I;x)~Dx0ADMjgiI7t&S#FJ%c={l=#)t}3;q@J@t>Jf!TEjb<6Z_dDK1!8S%obxoVW0sHgG+}72$dPX z)}H!7hc|+wZJpOv^YXreem(rC|qTgtD52uy@!R23(x`0j?WgqKwt)rYf28f6eZaz*+;FmlL zE}^-5jQ82{RK+jtu!?l>O#eSc<0mkMG;QPs?)EHXltNGCZaKtM%iZ#}o9rx9fekcY z{JR|GT(2F^7w<5jLM~<%PdGyJ#i^Y3x-F*pn*RMLTPZaZD&95_`VIIj&lk@lR+llJ zFJ>TCjxnAuK1M8%G4p)!4T36zJYW0<3ZZ=rpqKD`(GBJPVT|XC=0wQ35!Q;9b?}~3tNTaeDMzkO$2sB!34&x6E+YF^DAS=2pfY~%K*{ynG)7GCz?2; zOJaNT_&-&<6AioQ9pF@uHf!1sG?dYfXei~lzlkZXQ2XsCXx#Vr5#u$bu<|y7vYFFbY|&uy(x9EV ztSO;hO<__6hjgO$!8(k-oNzZOQB~@%_}NO-&-f235n1PBcIyscB%n*;jhjQpqt(0I zQH(W#kr>@ptTHKueR8yg8T6zRL9t1cVX}s&lx$(MWi%#ueFLzQn)oWKvYx5L4z*Zub6Gzx2zT7XZM9FDI zWpnLBPGU5alli{Z-sXc2X4WHD?&FO`JlcrdFO*U46+2^NaB>p>Ga;7pq?? zJ#@c^h@Af_Q3)B~^R(Z5X^cb=>3ob>N5=dJdxKbg#@q<|hFAs0aJ3aw(+yu%n6Zb1 z(Rz1P&mOyRC@*Ut9xJfO?U+#0VFsVrw;A=3%r77Roh`P$mIE4 z7OFg`hIUa;hM+zbGzPIlj8z~k9PvyV)t7myj+}2u!&7{!-##l(A?pa^LiNU4yM2tvV>dwpfv0A3h#GH1$WO+_yU7( zLDAwJ%*c)v5M$qgQv3Uh74H;(-CqSoD&Pe_7KCT6Kwvz27hWw%L+sByaum2%`Y7|M zSwNI{uM|n>3-5r^Xdm#CkVM$H0WXHXALDPM!NWU;gNY||oiUp#ob^Z=2U`#eWvn}4 z2N280SQEldBIdwYG+~zk^U+@Brm}_+*d7Hla*L<$VNDEf{Ph2z)X|0ci|!wkMG5T- zK;nKcdf*NzT5rTwGj@uwp@_|9Y!_h@@&h)8u@u6Li1lJDnXrY3HDhcJsmdC}V!Xt- zOr>zb`@%Gm{)oqsWL}AE{#bTdRh0Y;LHF4Few1T9#GWwbMpz5PE;Gil?p+W&!WeHX z_eX34V{&6T(ilAo!Fdeg#5W+6##F@)Rwyl;4z^DKxh%(NkQ_ zQl==C3sGI})DvAl!U~oaC#HN(Tna5amKM;pl5MO zwcX;AI(cdDiiz`|lvu;~ASzB!oK4f>Od^F7Trt6D=u{HD7yLI07J@|FXQi2L#@$RcqLI;5=f!|?1&>8 zp?98MF=D?M%_RsA~XyIkdDk z?t``RK39PH{%Z|u%MVyB8f7a1PK`=siN^KZB8#)kEGa`h;Dq6%pp7_UTseI}4W6ku zFc1L`m}e@hSJ6ymbAEbZKAGlgDo8t9Pe5C=@o-iE9(Ic6sqANFGmVs=oj6k=|SamY?<#6Ex)p$m^Ex$cNPWGsrX zL5Q7aj4kWfTzHMeeg@gJCZXVJ#yDnYo(EvF8RL+h6^M<2lgev4s;zZFCB!mEwP4|} zdZ-inZw_Xim}p^Zq4>Z*a60*2N41o$sU}pN8dIPQv~66nt9cthF8||RK$xfKu2Y-n z(yNR8I<>McsXBbnn_ncTvj^nbwf|_<`6+#^w;FoYf%Rg(qT;vl`>NshVs{{0OSNejxm?+g}-*Qv|!HbrP0>fxx$4 zsA7G4^=0UyN`{!OyqCH*#~t2K(t&Duld8THF<-{g2-|>|Gh=+Jz#hcjWaEHJGc6{$ zV~E|ASh+OQR01!6yU|Xw;BSQ8LToo<9RY)8TT@iz&!Lvp4Js-cy3@oJDRuu@P1SoXPxHrGR4%I=wSmv~*QV>J~jr^m)n?t3&4$(-u-GJV9qzISPvb(o zkHS=PIz1P`-_~X61?$a>Vq7CJ&>beLuCO+&6@uvzFH5^RL(ArQC^oyRy>y>`2yaDg z>YcV3c{Y94Et09d!W)PL1wyHZRuUT&^@0+Ri<;oiNHJbjgPqP9U?HlkTfVAoLAk*$ zjhzOsmaEcNZ_BpW3;?f62h06UeWi-%*=bc8ow?Q;t~i2gI-*4BoNB?m|4g=9Y+gB0 zw1Wi2--BL##2_Z*R7*sqe#5~a`*?tNQ#DHv{T38U1JT!Gpb*w$EksBzL?p zgYR!H=%IR-|2oNT)^czH@Po$04`%Ly`p8A*TDQ!-ad4NrD6q?|u$bnddgkoD+T7Un z;nM6>rJ(rJL-p`{y8v4JCDM7L-X+^z9*doD4u8|)a$_)y+94T>Q8N^#6`qM}fQ_0C z!vA4CMygcV{N>@TwyL&(6C5zXXlN~Yj^uei3Q}k>ZSJ`QI!gx7N{9a(4#Acd19PeQ zLVW{2O8*V^MScK1IvR7*cm4qM# zl-S7!Wu zhTewo%_V&2hAy@|o?l`Rzj~kW_5!Ojs2rUx76qvJhK<~! z(>}Lw?;dVckNFW+i>mxC-d0l!J0*^@iO%u<>`IbhQu3)Jnz`Ag2!xn`2d2F%5oR%!pd`%aLwX}ngqkVjb@|N>TdLi}?V|-r85X3Gr z#^;rcN9+(|=JQHsAh?df>BKS?dLhM(-kU|C1vv(0|$(RRW z8e%mWBe06~U;&2A}yb=d2OhLx@ypo*wuq$JHUWqSa@88mkH^`}=%jzGlpzR*_ zzKKV9)qr+O^IL|B{HFPB1ym+|QMgjrI#i&YuYZB8W>Z!c%{wB6J@S*Vl@PB-;jV8* z+k9$uUFvnQET7syHzyo*u7OJp=&3kSeYhGdI>D_jx*Xw_BXdgY{wyt0^=bvD#L}#F z-XqjX-UpmWjZ7u1!=k-WboN#QfmKu5?h<0Uw;GjDgMSYOS@S4-I)jwu6<7+S0xe{k zujvJSpH%k~6!$pHT2Wcr&`8UR*jC2s5LOMbMU0gttUh8>7z^O9oLeF|m_daEyM6?$ z17lg#;R6t>$JpP5Ek-O7FjSTmgsnrY5MxHdb|IFFv5|xw{pbQA#b5rVhr1Ja4h5ew z)`YN|S%6((ESj)?5c`uc2(`%8f=dVwU$u`iZ!CGrdwInKUo}yg=|Ig`I*iUFs~jp% z6m7Z0nLe|q#p#bjp-F8mdsj|4hY3bQNKOlhI0I`f7((1ykMrau^42Zf8PBOTGKrM@ z>XbaLutbI-!w*JNVTMPG3I$Z7atZVq`*dJ2I+1K_Q9O|>q`XI<&niOcoW(C|Mv$F!oDLiD4;H2B*}KYGnKd&Gv(_*G3oaj2lG z__l;xwRbRcFmH{&gC_m^LZ-aj(#MJOEeH9U&Hp-xgx_8TE;tJJQ~h+ghlmD#YQwy9 zAgceTW$(s=#a2JnyWgd6T-hQ}9NU(C%?T|r0d^?BLi5^4a}R3S_O8&f5F9M;wi>ch zyZ0nB`^91;Xs~3o%1i-fgimOYstpysRk<}1}CD8$+r za}&YSwE|~p6R>-lCGmO%%TbsHLmDjHC!wJZ46I;iax}p)b5>AqQ%9YXp?u z7pdlJjBB9iYOJ2aNOuw8q3Ff605zDc(>`k=hU=;^~u5ta@?iZLfM zMfL*s`i_C(*CJ{or{e(}rhR0gySHNig=yz3szx})KuX{u_X+^YFzs1I)fjKXd+MO> z1+k%MaNZgto)iTl*sM40u{S*Zj)XoHu(oF3F;E2%tiCxc`P&L&kW=Jq58!60<~TZ$hjnW3VZQ zI=c_CJdAOKcG^>y7zp{ki#0iE;t1`FDEOQ)j?ljI1h5;7afJ3W#E#z;-$KIJVF+wYz8u|3=U4 zZl^I#yV?XfP79Lt>zH9zztWhM>6W-&LapH5_)iovw00P=do(ir0a zYv}RtI$FeYL+4TJHurxmk$eLDYd54W^bDS zWc>hdc=YE0-ZQ^bN9CkYk+ya-|Gk*TEn)R>a>c`J<#y2sa-yt7d zo`(sr{3PYEAIkezmh!1!6Rn4PtwZ@PH$=N|wVu=7gTTZ(xL4fwcen~M#(!&MDC*^g z$9Xq==7q1>rfHfSe0lD}vspx;~3**h<&~)8_2{l#v2iP!WhRG@4Z(A zz{?DBjPdcifE{6sV~qboYy)E)V_d0&2q~*pj=s>rZure;XEXdR<(J*)d&N#$dt#1- zu8JjP)k5ye_Mwnr-EDEIEDXO(cg3r+Fzou@5MJeA*gd}kvWZtf28LJ%(#Ku#~7DI%;SpKQBJL#H>SN^HM84@>~d;Y zUT6D+v^Ju0dDUNcEthChUL6${bv)nR8 zTt&6Gci0qoYY>vvz%8heIFD#Pg(i(nS$L8grVthS*c<+~P8vgTE&??KR~aZCR#c1l z3dm5~`4pt+glPdE;c6?l(R|T6?E$_2y{nZt_3SND>Z|3wAN^%fr390+BnvJ_n&X)f z4ckPwuv6D>O<}dSH5hSRFq?XxaxDhA{-iN$IUyG&!0nmOEgF;ab*4Pg!BxzOSG{BZ zEoCvk@@Q#Sw4X+Ooin!MTdHqDFxm;p)+yt_+;f5z-9CNvxg@&11#myh5 z(sXx|=hi;ZG#ehvoR42}7Xzy*K6$3$Z<jt{hd!s=Y)=MEbgJL%Z2S_@Hi!V!nzXdhr%~c(MoXn%{D8+POdp*m~BI+ zy)G17>ogYrkjubT*0`0R>z2y_1m9?Kob6X<>xyuUci3%eiPk!-D=GekuCy2&+nDD7 zhi{S4OGj&ibn1#Vv%hcRU}HPw9SQN$*dWz8iw%xE2dKnAAIQ_(LifAS9N=jNy|1B6 zgEOk=s;EF1HQK=wC~t|nErHlt#&`};0kPj1<2gW0#KtjZo&z*Oun&Vg2WWc%uojH* z9H1v+H5lVLz#oW}1`G)@N8P3%R)8^%y4{3W4#qg@cHae@1H6NYEk4Xqw`nMNpD~WQ zy$FHu+F8ap>h=y|d(%YYrmBCtV#O?jSzb6%RzVfgryjQ*%+UPX@S#4GkwxErxTCPg zE)uq`|4u~V=rl2*h8m+=yiTaiU~UQhB-%Gqe{s^oAMj;d6{i~xnc{6TwZ79d_+v*m zwUVyBgFMK(Yc16`p~-dX!vo(xzz_rP1)qR@KLGUX7_Hh)fZ%ftg4Urk(@eK%k=2Np z8)NB&Ekx`?D#`66Yz<-$8Cyo!4#dtgHvM~oBdWQuhLvDJ(*#mYo%HelEv z!IYsRW;lj155hbU>&4h-n&jn2tQliZ2rF{7E`Tu%UM8>%zN92$M+l2X%$KnZfN7(l zy?R(b$rt0xtIfhb9k(SS_0lZfgNVqjM&$6Dco7kKp<^Oa?T46EN1d!rxI&!~{PlzA zTnQp|N`DbCb=80dk(_Yj3-*!?0cp+&M=-%?@CNU~1mw`)(w!t8QEgWw-L#sA#HPAx zn0wSQEUV{~f5p;BHQw840NP;Mv%T4X$~nMOREt%|su`E4SR+2eULFt=UE(KUiiIx( zzJC?LajJLK=J2hH?I#({NBoCphFgx3H527HAhJv5%a~-6Jf;57$ zthHIj6lC;Y^!iO%AD}8v<^Ek@E21C^k64r_!9<}mE<h#kB_U;-$K`_n) zO?kBol|LV;%*f+EFmGwM#Ie9quzU5K&MUU zpSVZv$&Iy>{H=+EEamwU@ZZ%SATaLwEFpINC^Ur0f4dcr{il z=nNl(V>LC_>BajGHgsbb{4#Gq$9O+FO_k667OO{eBk>oa8xGZoZZsIeLaX0c+ral1 z?0tK>F}RwYqLzs!y7BoGy{GdV(Ydo~NO;G;3qh9VHksdq*v2wW%OTTzP0fjI+&m05 zAG`}+YhfGzAl88~W*e^&tH&6#jjxDBGG=BQt}x=XLJTt7$cuux7-O~(h}f5%^kQZk z^$~jtm<`)#iP#m!m~C`L>`%s+Z45Y!Y-1yX%r-`&;C#lIZA?7`*hI#dZTx{)|DD7( z9>8KO4`0bP+&-hy=s#h6sAYo=EJROys1Rjj(XeZuP}t)I30v7lB^1uwL2RSxe91O+ zh2DsE&D5s43$MkFW@@x<=4uc88dT@p4SLt zDo0Q=chd(teQE)I%Xu1RQ}#c#^?nau z0+XHJV3aih&luQCWVccybDnBtw)0!EMfKKd@hVLkfQgTXDJpJWdc_&XV64yNkOf#E zI6((OqT(QgE=UUlk+|e`ur>gT4!fcHzOyq$M0M9-ak{lyGhyMo50()5O}09W+kINv zda#+x(CNvbNvz=MqtqauoeD#e3u}dIkv+cQ@~(f9%8fd|1kQWg6Kf$0 z*TqgXAD2{%C5G5YXmwZnQ-z+Xi<+XmR?6*r+FJveFV>s0i-|<>YT#=bw zCe`#5W(t-~@(J5Pv8$}ZQFGGr)dQN{=D4Wf^hyr7cIzO6uFiRRRY68k#NmU^>%_qh zY7M7OaNgsCKCbm0pNRS$!O}i?j9xvMV%@hq7MDA!wVirAmaaWgSL(57(n)RU-R=NY zXT~F}D*1DVn&Qt+Y9XgF;kL14Z}yX9kd!1YI=6<@@WQXc?KFH#DRSdO`JsCc$&L5t z-^(6a$6uZr#jCep0Ru zGQJ@J6GAxQB@jYR*nxOX#_6muS^?=;|oCnhF>xENyH8A1J2FPmOau}W9);X#^ z$40;=Fvd~!c@XQz7)R9yAl8~Oj;b$?SZ&5Qsy-6mQHC*&s;_~V|8lXSyBeYs#l@-a zYEADiyGXx_-^GzYA1^J77C}AKd@-Mj+qKlb(2p2%-yFuL4#JYtL~h^ykZtM;_ENfGd1FVZcFS!SM7A!s%w;Xt zepM<(TxR0FOc6azDdyyR*G=@CrkqvNwoy-Y&G;ayBq>2o18;s1o{JT~$dR0qKLcCV zW;J91CpcgN>^6fD=b7yfxRtN-Vny#X1x{E$lcW^%MzRtFwp{ZZBikQ{j7w{Gv2n3d z)+3^>naOy=Ct4%7z-L(PfXN)(1Clg>@3;zDaN{_Pdk+?dU)8!!vzI!HSxb~({M**Y zR-%=Z@ZwavQ!n62>lGayw?yu~>IHT0W~yR~>mNki45(t)YafK)G9}n)+m$Radxqj$ z=9&icQ@Z=>^E0^^vN8+`hrw3hy*0x~EK+<(5Uz$8UDCtk8QVogNHm48lK{ z&e&eCiQXG^P2}hgVJDG`T|``pQn1N(PI!3L>Jpn7fDqYT%rTAS_Jf?6V3kCL!?J;0fpd_E>+1dvp z0B~P2G!ITp%!Gii0IPoy4{v>)TILLObD1l6NaS+V%z;lVeLwFp{0aT+7At9Tl00== zoFAy>QTDE9CAuuMfe>i#z_p3EZ0(!XW#Q#^S9oObb8aAK=CWf@uD^6~&-f-Fc9^l% z{JozM!SxLCcYF&M0ydX1K6-l%V&fU(qqi?3)>mSdqqpxN)`~IyjL(EvEynmWzRZO# zaF0g>gZv$zBNoPwF+O_R10Qx{jE~;VkJyJh>^q9IG2OxQ4t^bxEXZg&y$*YdaG5ucdsDbd3?q?Fj|!m zJw`yhd|gi4c7{|WhJOzPS+l`>T%1(o<#NiWnN0IF^`O;|+Vi1|sSs0->jf=onKd_J zn;EM~STte_7z-yX4zbCM736sN<_HdC(3J!`gQ?QmF-D&@MfXE2mazwSD#i)YheUGon6k|8RouX&m#JWvzr|2?j@Kxy_#F)+S zE&fR+76+D6LhE$sC0k;V!3i;#U^FZQ6U)S+D%>H5#G-uq6zEM!ePaLjKCt{qD1yMC zYK{7seOv9i%r*uZ%zmxK-Aqvt?i%%m?G5bO-j(>{o#ZBXCi%rai5GRw$!)8c6=-he-i(SQ3u zq0B5}XiqGPg=OUMwPhL0a@({ zG|PQDW*swqQSN7w@!8XcWQ_E(v)NI(ej*v}F;Fc@GLCjPcZ4CvhGaa9Fn31!O(q$A zi$HJmOaXDDetfdKXtWc~%Eq5WGS1C_&WVI`3nj^TBdF4|Q|(m9a{(m`#{{F{(khDz z4TS|Pyvl<~#<1Da0A8HwDjx1s0unY^?4SI~?(SK9UcY~`t^H$;v1>fE&VO)>WZ)_P z2glfc)zb6VufU)79HW-FFi_2{WX)$4I!YuXhO0U7sI_k%2f^FzNQN)V#e$rfWE@60 zJCgB+^AEC-n~QQxGEOH$BTt$rO$bMSTd`mY0mCE4I|45{vec zqC_q3`xCW@Qtj0AqZ?6+qkmBT#SW9-*w4SUL)La~2Z5Wo%Gr(6&dW3lwOBe0Djx-b z^yoLtw|XEJ!WdJFJ&1WR#?;~%V&BHmvu0{>0YQyHrWUuR0(Olt^Q|6;r836UA~!y} zSz;Dy;g8q?#+X`!AvT#YrWS^2NG%33$kd`b3btd+e5(h(7|*1Kl{NDfVXYA>&lppS zlhftM3P@sVaes|vXh~{O@Fc2}zHWl;(1M2c#=I_%H7g%ci>D`0*uN49Td9R<3J9+r zOVq-#3sH+JhlpB?KSb1`_93DcUk;+KNotXL(1u!km_=$3vfnyv7UU;tF_qI^?;{l% z&%d|tv#Ll41cjoCl;pIzGR;CQ62W+A4iL$2LoJ#h_Hq31x)Zz(ZofuX5pJr?q^0(@rRoz9cuozUoKu8_>uMVBI<~5a3+WKTG6=D zP*KS%vYVQ_W@u>}UJ(>%?w%a!%qt+220nI%Z8Wessqmqk=PckA_>*{rIvTobSPSMA z5mTu0m$usJPa{s)hY3c5*Y6hn*)yMb#XCv$UG|QX*3WaST)YTKSZFbRl2=qYfY0kk zKwyP+lbCtM`~J{ID_AS;f3h{*3+YyUaEH@40%*5hbE|9akfHZ>TI@!owLpdfEZlj=;(*~Mt zdr30xZeVx8xc41wM=b7)1nK!w+H#9|GYGcm3>z|AZPqS^U}xA6jLKVW2*!J?nhn7? zu*HU8Jex=j;klq^xjWD7gc?u>NCd9fi#sni1vBr!s z!EhW7SQW<11j7TtPzIS`T3}weDKE>~MI9;Z|D~Uu4u+xd<6ntjRBucKBhN-67^xeGU?grJf>CJ$ z>Y5}N4jXI;MtLv>_&s;GwN~B9a3z9qYAmINuO$^p<==1CSXHE(E2&6dPP-=4ECk~u z3>+;60`qMM#vH^-GR6es2x7jBF~K;4m@{K$f^i+eH@zrBCK!(f0d|)$CK&%BcA7CJ z82Rzp-4e49j3S6FXN(C(8N_BV#snjJFcORr3^KupL&0v0F~Mk#FK)~j6O7J?RbfmL zjFdHE)>1VnA$kNV#wpDp|9uUuq_r7CBleW`W+WbH`wW-&pWwthNv7Y1KN-C!(Fd_f z>DubXQb0S5ka4tI+xCD~Z)chTxJ`KKY`lxCdduo9syDCRta(#WdYSr%P7M@k%hbZU zr&Wcv4EE_jbAhiD}r1ub}|c-VmN6`Pz7c7KLo9Kx6o zH*M=pOoY&m_@p1PNyoKUup$Agu6T;TRp+m;UpVHjm)07B>f@4!+=a~=3<80u4>v6_ z9!bYo+&3>!)7>F=d~$@Q>muueRda15M2Z@g^IIH1_{yXjUSiNim%_gKQKX=?SAs$& z!&Vetzr5cmQ@Acy4L+w~5Ta^-T7jzlLniix#HP#D@;ZGzk+fWm%+-De6dKRChuT%o z6c?7O#dY&WiqFg8v!bHu!gmGidOlqtVppgubZI5TixqHgz?u@meWmKx@FVP>g4erq z6c==2;j^4h5Zve)wnYA`-GWp&UTWe&hyZ|BOb$uKWuM>Rr#Th}&2Wj~RLFBF;9Jt zgkp6do#iq7$aAdNwo29OK8+FStJGNCxZ=WVwHm0qP+XK*t=9Ig(;2fI?FCwmRhB2O zm+dTOtX50w^T1{_z8;$_KIt8JZf!t$adEX8P=3I#Fj~&`mjeS8Bvd<+4nN>q?b+DQ z@M8b{*oY4>(@WT>{YHu4HE^`f_$X0zjoMk~-$|@k17Ey5c7lBX)evyI0xH+kjW`+j@za2<@))|G|JdUafIMM#fB?ZvoN zcxXHb;l8|MhK%1Y&|a)wul|z11%zG3Cc{N{ueE|3Sl@Br{SclTpvH}3MClD`sg{HA zkO3(9B`9$`{F^}~KidOJUb|0L_E9)+1Ol~AG)T-I;)>_$8Amv4=VP!4>7b(s2S99B z4lhWHGY*GaA{skt=NE~c8{osAC%wh34Qd77Yk6Q2CF3&ugP*!Nj34dgFw{?LktISm zs=*y^eR6itX4b)0!1yL(E6Ajm9*z)kx%C8uM|goEUq&r3Mr*=+;y^$ZkT*sT?~Zrp z;9Y(m3)~zUBz5;{kUZ8G4m^b|h-z5K+P>0k_*EQ0W2?8YF&lS3VMs%5F+QJ5koWig zBvA?^%%)?-C*i+I^(((WH);*U=fW#LK8B+WV56`bxaLV|j@rIYpR%XbgrWDsGf{5O zgOT&qnzRx9HbJjvRHa_eSRmGJQj7b%?1?%)ImB0L%3=AhCc21co7AesL~D2h?D362 ziHFdysQ42RB*oWI9|pG;@tZ*({*4raHmmt^Pi+VDRmF2^MLZl1uK8^%=5JQ}h8&c$ zM5$H|U<|a4_#28Q^L9gIK`T*oi&`|n3zQd$JgADiq7F(1o=^@&ZL<7V`k)PP1to@( zoYJa+Ua&VRPj5jV76yMZ8X*6Kcc*LbK~+!(RTPA={|W02_#MQds&4UmxYX;7Y>myF z`amnKiHBQ2OB-n-dyDF)zkC`r7@lqnau~P)o+eib5wTTmr3*PArf*f_T*I7TLKV_o z+}^5|_Wl4dfYtY(7lJ>74LZ_%J0p=Oumsysz2)u))oqz zfp5JxKs?&6Hh1j-pw^_Zh~5F4OLd~eh#hJX@1DIVXE7UcGVfKs6T5b(-F1)sM1h@Z zv}?t};H^rQ!e%|czJP0r_U#I>F3(tx%Q$buu<0rdwApBw&b zH0*8%DjeR4Y|-9G(#pNF(G~?1Opk#mMYtIYhIU6ZyHt4W)x*MZw^}a$0tg3zZbS>= z1^LB8ZGBN0Q$>T_YJlS-(Ra66B)si$G&J||tXSxwr6GK>>}ze}Yi1Wo4^YK|@X;TQ zMe1%fUvMZJYH~t`1EtVc`O1q^QEXwn$lMK8pIKf+>{0tAB>ZZQ`OAy|g)u(s3 z(gduABX1BuNDl-c9B>S0#^lzz zmIvbGIlyHTJjxEuNtnqUQ~SE3(c7+eD{H;@O~ho z_NjjT796!|KQu44?e$>nWatDra1PTK!P{Sg+@vufu@0U|8z3sjv<1TJw1dbJ@l+y7 z2__$eGtad;^FF?gk}zWi73j`MyfZv*~0nSM5+pCV$v z>Z${!hDY?GA^vE}g2hR&IseQ_KU?z8S8}SRZ{1kz*bgsj%P<`dr*Gd_{JUQ*R6Vm3 zHENM0>*!ec9O8`d-UIR)%5VLJ+!}qP<|9^?h|X(hl4fx$G<0&7@R&56 zIHD$=t^8Y8+}uHwa5&yNnaot|UTQfCI7K9*fU~6c$$el3tdr@cIkoJGKGLHl5s|H_ zi|q85rKyO7B1nlqC{hxXl%@!^9A}`C(hs2*QsT4Cn!6~0G%!oE2ftJ(VPKHGgn5Lo zj|}lf2%5$N5QnfDLg1N^N`Lyus3hKs5m9SKKoS0AI?Oek2HBg zt*P}N`x{6G$3N0w!qgEA& zBU=*(h>IDdQwPe@Oh#T};v|Ihm!(;%q%1M7+vUmre zg2ENvXKAPdP)cKj#ON#ybpT4~O*+h%!m~A-Pz^cNS(=~l3%rj?0=FT8NJL5PV^JZZ z*ARvJrBh%vRuY0CZy%?oi*g_*nsow=x>5VRJ_K zng!QigX@2API=S^Ma1~iV9RNUyb>=tJBH`*m;@xUlEa(x-m#Py*9unBXuwZo`5RdV z0|{#1ho9o~s0hGrvMN~`Giae^Pe3R%&aC+WISQ3Kk6$WqHlzZTnyq<^rEvPo(#%6! zDc|o1Mao}F%65dpwvbj6&N+mNHn+l0QFM9yQh_n?2%$pms2>d7=HqTAj{d+?J^ti@ zCIr=jZO4|E3zSbW05$SQoe_f?p>64RX5)iaN}M~1*J z4S~EY3eH0jadeZ0;Wf>~IDKV=V;D}v5xu9|9Y^~KFIBbz$6wQf2(>v-kNAginorUuQSA8IjXk2#{@X$ zC%;md-U7!ZeW0)xh({LwCEUTu1Zd}l)FntoQ{`1WsYNJ~1F7j&V?RLT+mvkmAwq5% z02y12dL)`TYYxl4y?OCT$PzEIY>P#fi9ESoXJGe1oC@C~l9H(aC26%t8ib@9F#_v> zNJ>G{S|#b6NE(NvF-p?#-u_hWZ>f+Gl0%i`yxy$n1^9~E5v<$=U8E}N8EbPvS1V*r zqM`zcR^y5KAkP&t7Bl7DiEQNsU1*bCPa*YbnWKM(Y+5*>rxN-e?2HNY6o6V% ziOaztQkR~7!cF!^n^>Dmx^PzIqV7Q*cUWk?4TdTk)I%BLyzFIE2FP9p9f!A$3zADf zFUjdR-;gC<(lt=M9>!)~(oJZ4vM%I>FZ>{ouSC4fhp=spade$j=uRV{YF7l?{|`Qy{!8&wEG^hUsbSjw}qR!Gy{e5fjx7t=t_oq?Zxy58?hjt z)-ZpmC#J!c-zZu0&Cjg<6hA~7gxh?O3hl=he)^ z%qGexzL z*h?cja81|1J#-TGg^8sgCmMMF_W zW>H|Qgcyme5)O%>-kpmuD+J5q@ro1d18hMOy1nMKh223Vs?`?ec2n0ds3xsFO-7rE z?Om$pLFY4p*380s-P8s2ZVLUQt#(X%Giitby|n&^uffR`aWs_NRb;8kmj2?MdVI1*yWqJh1C~(e0&|U?*JBjOINpK`2qZ{ zO+t|-Xqst$4G1&t6=r-^KjU;Z@|G@8TXs5Mh5D6!xvLAU=7sSRt&HX?OZY7pt09_n zuWFw2+71nqXpelo9J_x@*SOWYwOqWd7udxs3-OlfYZq^OH)5yhd3DkiT4!qvP-6OM zOwfro&4ioaK3y1WfrqO`-PTnK?pId`cupT7;80>13DNZ^+j|=VHYf<~fndLdbNq;1 zz)q88`34to@grQorznQW@~P5Xz#Zoc0naNe;1NHvhIi1fn*Yf9-O)8PUCo7n4b=oU zK3A~|_{lU<(%P8S=O>M$uLn(|VlZ-61*Dz{4Ji)-(f74}yR+ zLv3R41~8U%Cansqa93ButJPs{AsV7wzI?*k%=PdM>gop6aMTI~^{hrL}tQ+P+}=2B4M@B-wRjt#pk#Dg^ln&lMfLr>j)T zx0VoOTrVL=v5*8oZpR6_1a{!IF09vungT}+9J?&X0c`KAP3)a@9_G$a-@{l>1}`Qe zMk_KC@zwJ%QO5E}_1)!y6 z5=P3R^ocjv9OCS_dh&Ddo`$-eU_Lh|NRD&$!sqBIb#pYE^-x#McW+PPGqSk4##>wo z@_jVB_)wP^e80NjvQKv*33BUsmzuJl)qkXm^k1qVoaqJx#b!3%&whBM3#t}V5tUl~ zdLob$aWu^KC^w%j^5xIw0wv|YDn8ND6OEU@FP6u$9^t8o)n=Ce2&*#|9{Rb@<%>4P zA!b(ZF|PXsD#;f)SB)QW9qO^JWMnSY29H#J8p8E04c44H1#l+hU0;bjvLYKz?A2rJ z-YomfJY`+5Ydd^4_x1~qssXF3il$!fLJB2THe2k?WwUR2__7-xHNJs^6pVRJ7!poF zvQtl81G?G%;d=G~f$SH6);<7zB zS11BwXVs)mWaVtSaL+DCKR~k$l)1PH>tfSIl}L{uxng4q-JQmunvp_rJj=6XHuUpu zfXl^{#CzrPXpe9&ZcK?iLfU$fHyGJHo6g|BiX0FOH!>2@QEF$S9epaYl23FY#ky4y zP59J_HGZNiQ(oK2`KJFkdg}+$oVJ2`kyR5mfqKC{!*Onk*wQCDO&KlfrcKM~q(J!U zGllfW7hG@4yDzh2PjuzH549jPl;4{X^}UFGVhuY(AD{JR_tqsRSe%zNmTWqJ*k}7Gw%Es zw8U!MdX>I>_FfM1Wuu?#T2^YLpneNf+69Lj4}17vC&gwOwt$n(E!{tOC0b1xF7H^w99?X1S_RSk9c`(!9bylYnpJiOTbQG(V~$o3k`)B8xe0_bIHVGr z(Gjgq$Zlpg^xKZ|4tSUcM*sa}==0?Zbc*Y!vTi#fQq-vD1(Nbn2koMyerO|FbsSB^ z{&u{tRnuuq;l9{#3>)|oUYtu6_VY_vk#@`3`j@)a6<1v1VwPuM;*pph3R8nMdqa_g zsYkIW&;$eiq`+3`6CPqyA&-T>(v|by9U&;OC6VYKacTD0#(KZfl`dIn5a}j%MoERh zmn#%PfkNdz%h;krDCt0n z%~(!d*y=I26rWHgWFrewkmR=pNl?3U2;^B3HE4n%6bav9u_Auzu_AH_Pd4g}uDjRC zcSuO@47Uz@a`Ne|?9LmVub&pD@QE*Cdpd@c?rmx2o~vu(dpeUg0pC;SuI@n_F;V*- zu|Bzwim?*22@ifBgnMzKA32l@n;DKWivqUEDJc?+c)|31w>wJ2}H zgQz$2F!~;O$v)*_gufFiDDz4n%EYWFZ5^xoL02N6_CoCYKvQG*^m2zG9-%yS9vkvO z*HGIw5&MTX-QKcoA9NKxN+T9;`I-omd7&J8@P#=z>mxSUb{u32KI)oQxWOlM z*lXo;g)DgkwmHcL9#zsiE9rCIuwtLk-ta6IiO_WnCIAVabSB@qRuWi_T{=M=g{iBS09$HNcYB zyK{pW2ZK>y5SJ0)TWtQ2K_sjM3vI0=11Hfklb>WsaI+La(v>YN=`(bqiOdFnhFspf zX6c`GZG+2)2o85@LWS55#lXyEtzvnfbqT>oN(zKGaRT8iYT*{6&nnjai!M0Gw2Pa* zmOL;n-%O_OjND80md*a6>*l_D5V{x+VXk4hUvza!Jn#{EjxVusJ)cEPfm{X_XU)Fq zLi9TTm-{M-^@_9azUs=W`uxTgeZ|)cU9PaJUv(oYj4Dd3*k?#qgc6rS!pfJsE9rxj z^h!DG+kCjvyACmy4`FHHL^K~_g%_}abNb&l;ffQmcb6AMt z92Z3=5=ARc_Z-7?;&?1Q&T2JXGdPJNl>l0JnIsCr5~6qqoJ32aVO#GnMI`FK3BJK`Pg`1G_4o%`Gi;1Yz?qMNrTYb08vRzUWR>WN>x%rd1`xnVT8Mg zllwuSii_w6%^KW~nN(7k-gJy?DTD&GmP7N|aFrCLD)X2vR!QOhjg+#!ZRi+s9xY>6 zR8pxD9aoX_2mPg?4^rJi{ljugKCqE_xk|xR2m1@^+>uxKu(s3UQHS@Q8khaZ<&v=P z+y3A5?>nm3w{l-7UUyfit?EESw#8Md8r-5^ZvH_};tG~Dum$cWmf}3+K6p6DRqCLs z7{l7Br7&%u^YA(wVH`ver=;(&u_wr_irL3JHByM{ zFL-javPLo$+d7qW?7dv)Z#G&Z)ijs!5IXjum@ortsF^SW6w)7tZYqD>KB0T(x$d>c z^>O-lZK)b`CtR~A4)qZUj_kY-y#0YF%XtezDd9#J3!l}WrPC0$Ci?t{!g{r)5Y*xmEMnJ+Lh189=To5sQ*cHT|$4X%i7 zGV=LeDg)vrVjB#_CT$)oqLoT|eR7ATSf`{;L28s%3Q%=^#F}a)gR0siHWXN$ebFQN zd3_w8xs6qp7;3HFyGgYC@gW+P-6V+;s=g!hAtQx;y_at(zjEXYgk{-a7>DNL9MIHb z=mk0Qfrso4N>?p=$i8Z&((TQJg&T@zs0-{+oTq&}lROpkG4hkC1Z*zhOGYGkt6w_J z3V{3)QN2H$7e1I##5-0iw+vt--6g-^x#tDeGOlj1p@dW$NODL3TkbAJnt#0u1WRm; z-g|Jg{E(bxOaL-~ffm`T#j?)&{R(L$MoVlGE#~dw*f`9|vEkHQ!y4$6J6accu>v9A zho4!@k!L7L(ISbsFcwLbsR{F_P!JZ4f~eqL0FW+s`b4@TJ`F|3pUOXAV~a_Z64%m` zjyRmrSt4Pit$L505F<{d*%r~SbOlONJ3x0nL|Onr*W*`wb{6(c@@?Pb@FN<#y0M&M zQpp-ywa^xtfUn`N!kS`BD7}OBf<|2@8PWv&8zd`Bc(&!Np-u`2?Rb?$x5E&Fj^Nk| z!nzmYvik!(J@vvPghj3N@D1qUDKy?%(_zGl%;U54h@PbsU@Ir!VH~OxQw68>y_IWt zx7f)8qx*8iz{_m-8@A=Q_$~9NMWNY6AL178%-g(`eehX;M>uTKMUtxd{6f$T4*;Ea zg+sLpbi0J^d#(#dE%)n5aIFXNAAFvNLvr9`(;G;$;R#e*efm|pW-d$CsslRf!ubgg zjYnlQX=unW4>z%=Njs%y!M6Jbj^OdEwA$rBacCj|?(nl9*vPASODpyWaTcU5eq*2X z1~T2g#mpX3p!woWBmw_0_6K+0h89{im(~#rW@LgYAoHUiFOi&1&}@y24D^Shzefrv z(!}aWZkk3YJ>J>~r#6OCY5xEb+5SWT++hF`?nDRzzySid+I$#eoPKB)l7{l6DKACY z!--VTl}Bq%uPPBmngzhf(%i@YIGCY1c|%|v0#O%`Mt>+OJ4^G5Fp&BJA*E+&^eBx| zB!pf_!A}c-uCxcArRjs8bogTyN~Aw*j;7*gmZlD6!zS$%AYhAsArgTWfmHef&EEJK zZ=Et*#JUmOs;NPNMWYe10f(Rl7L8uSuAJoDIENT<6>61-*mbR^YW5v2a-6j-rz@lA^sAkDtsyO>rIyIC6$f5iaxz(SSWH? zM|+WY*8q?9_4si%jeQAnMNziFORAz;UY*_ak{YU#tFuzx(!dVS)i5SjzmEvyZZFVX ze#2NS?tziFl&lqIV0JVbs!W&EzhXOXi`b5fQQ#>~@CU!JYu-|Dv4K};I2xg8sA2iu zQjn^KhLtETB^G;kS)`vrl+v=ZK2klEcNOO0D^-YC83Ok*@-*C>MB*2|N{6W}js%TL(#R+1r5DiEB&sPWEww=t=mhoihfdA z)#>XjiNe7p*=Rp$K#3w`XE6<@BOfT}7|0kMgM7AN_SR2o+I)F1m5*cY*xP)LRT1%Y zgf+y%(^0Q&DQdJod}!5-r1Twuar!EBnpRL)!#@Px=LfM&vxmA|b7 z9`b7+P@ErZs@{heI{^g)>WAk?s$`aD?=SpWO;@Y7h>}@BU5Z}P(&-M(2t;8x zB6y&=dl7?oeLC1{(HM~d4imY&Jun+x_Z8l)cUw{$zw@rt!%RMeVKpuUB;KNub17fC z-*W+chY|ryQ-GHRFj-B;RYiUHNDce31u*9Xm41}YTv{~yDJQRZ^mKrR6X+^|(oHMy z!K6TGtm;I6b}>*YubMrOeGQZXN({uN*a2=ft)?G9l|1GYRLSbG`7)~#Bvn)W6Uq7p zNwqvw{Yi&$3*dG(`!z|_32qnn5ezA|*`X3rHRFlW z4*I(Pf^z0@xT)$F5(Cj!zRbVqn|qo?1WP^5mp<{@uOpyyF`XX3HhJ(~!R^vlLH+D< zM7`KVP)`TS2IpR7dR59%f7)!+pZPcE&wOecufH`oScpFdNE((VOjA;7Xv+K|s(Bgi zsHWdfKv2&6#%2tMAe8kptB36)r}+f}dDwk6xuoP{PEm?j#)}Yq_C*o46g0-^vD$bA zZ=6$A&wd0RX6?>+p$cA-Q+Y=Tk1Uv60sJU*#)kQXNX4s+?M?J!_0#m6NW@0X@2~Uo z?UL(4emB&t%?U2N8RuF15NTwU%gMwE@Yd9lXF4Lk#nVOhOuNrfrepHf<2=)ob1XMR zil~{~L)o!NH*0NwbfxD4g;}Dw5B9&5cx>v#4d@fB z=z&}t=M0eW3r+=y1@-Kbv`^d#>$bG0eq|=5V^Q6L*{EFc?_9RuQ&WK*C@ocLT;iip zhUQ^J+9;Z>Vg|TxD*3%_P><6Rd9(Li5xVfeSQ~}Xoa@kS78NQ5=xTSPRp0ss+1#GB z4wZred<{f!wmhLDl{|q4vMjmuX=VII9L063Phg6RU?i!@otd4iMx(L z*jB$0=|qFc9W;oRHUtw|S~}q#k7zA#;yMx^#!ehC^E0L68>Ld)(!b`38V`p$*H{BJ z0_o%o_DdNlsMfZ(yopB<$Te>PaoTnLEdFRVq(iPN+p4G2BN6%?e^|}1pb=J)wMW{}6V$%##b=90sY>z<-X>kk|;b|(m zEXgSJt&mYth+<5j{YNK3kktxXvb@H?WAYUN@Tl_OeR-ZK zL1Ze92=Ts{lCN4pzT+oITrc%$RgjcWCj}Z1PQZGa)0kI`GT_H4nqZ{qMM)WmP)V6s zCMOE=gl*~VE2L4Gb}e(KK^)PUTpS5F%peAtgMO5=KXkE&5JzP^)zv1bs3EZR8^J4c zF!5B3V{ay4tGi4|sjR=R0*!NmW=vp3%S)wvYQI#wz}N8`lB0Hs?H=31w(AP3RbFZ| zusU`w?MBzC`5ldfSLV0ahtGFCT0%H`ywMvoTiNml~ z&305l=y#x5D*@C;`ZYMg?vw}59~?*V0MV583Vy>qn+y!Nk{%;LJVL+S2;nLH3Mh~o z^PZ?#Bx22}Wc|=nR74)2M6KT2;}MmrA90*l3ixS&K4x=Ya4KcAXpSO=L~}Tb1Hxq_ z{VkGQQ6!4!M@67inVjFHYEpR3AVGU90;KXg8$@>*Dp^1D0{&%XX|^F_5xuwHA5>hn zW(}gsl<^^c(qES50Di%T-IZ;I6N?kUDZtIrT&0}IatR^%cUhVMr2B&@WN`Ex7*5P*+N2M&@$U=WvnrccR zRVd^sNlHpfgyaAKeFlDF$^!Jhln5Q%kgW|Sg$qZq2yPvvKy#T`V9{(j%6_gOwUzea z*cpEwTi$krJ*yyv`lQm6ISI z|Lo7rZ(&hCV)Zt;mS8@Wq{v>!pNl@9=LbTRK0kCU4DA7j=a$8%VI4gkhTdj|WeKMT zrL3j{T%zPY@6eNFaWeE}_miP-;a^Irq!lXRo*#_$;W6xUB`K)?Pfw{k_*oIi|D1;b zuYmDWjIB6y4X0i|tuIn&WP^gsSNJZ#579JwCLRJhea6+vb6xgKwCAiZRVD0w+!!`K zN~+^?ViF4I=TyLzOqLZTP4lTrfRlWAUz8k^$$D0n#=3VZg;J~`$xqm=%96i(l!*3! zvg=D_=^M3=-}lJ3g)e+gJ zhSiYD`Pjw+kmdx)+s#(hkc>Xd2~gh&uw^%UQA66|b88F$-(z{!Uexmpw$&(w`K%>C zoD(1;gFP}z<@{3spjBe66Kp~T3#%zvRbPH#TWd-+;A7$ki_9)j?tv%P&`p} z(6vH?HVSYufOfTei&NGGhY4acilO(>hBcj%_t-5C-=0Pwf5~RAYDv{YGN4uV1K{!~ z00!bE96{dA=Pb6i6jt|pCzQ2>5>bG%1fYTzh@B~4`NUSyhFAC4;@VOz*EPCacDc4R zImEXrb*kIWi!8Fsi+nnVu=WHST}LXTdY8@S)sdnDavz~dbk&&r&d@~56FOCJ`w_cc zM=Ix9-aVIT>q@Ove$lK;U8#-q8e9`5LEg^(sw+i$Y{cYJaSyj`XV2Wz<0Msspt@_)#Uv|X}_GkjHKloDYD6jU)(7TJ~)u9$~2nF7l{4 zEdQcsiL#~0p}Krih|8@gk#C<+d_NHAm}|3SZ`@g*ui%`}5Q*YcfN3C`Twewd&u9>T&kL>NIcwFWi*925@ zf~Ep$pYUV0+J?@#@|(q=ayTNf8Bft}{(0Re@z$Y+WO))CKwBF+3*}fg@|%Bz+Dgxv zOfV$L74H&rY5fs`Pl7cbPhbu7w)kDPp`ldEc=e;Bkwb@~k!x1*MxNvKQzL7E4TT!H zYAp+lky?CH=L)f!4)uV-NES^()P2LMNKI#;xk67 ziTUhRtW>JZGzAduLM#D~_+haTN~#{{(T$|8s&j+c%to|=y_)TBB#o{zG9NNyNrx;-$jZ-V0z^6Yi=ALBn=JjbR`+Uf}Be2!Z@+FDK#JUAujly@Ds$F4}3z2gptjWcO^4((8U& zN!FnWj03weP7@Na=AxkR$`=QPiM<4cNy8N2Vke+CD2&Z`QMl{f_#daukCSL@j>_mT zwm(q`TahGe#p+e;aU8bufBPTPHIb@EO?qqRX#3@y`iaOc)(-B!BaV*1+Z!Wjr9^J` zCmYg4iY%FOK~THw^Ba^*$_+n*mU$A=o3X9wn()9X3PvxEVK;K;da&nBq>{S-;T!@E z?-h}+?Pk7Br3R|HS6Ta}SS>A6j!kSTMTd?!3AZ?X!Z9zo|7MH_AigvFlB=nhy1^Ye zw1=H)Dpk@P0k;#_%gdUw{`b!dihMqSBE?|-)ScC8CY37L4B4pr#mhae3Z;xvuupwt zBb!MvtrybN4ZDcP!A{KlEI4j1mj~BuW`6M8^z^lzMTeGf7Ckv<7jRPUzm)Faaz*Lx zoD(E@eRPO4t(&5BD;40y4-Q~LH)o}zu3MmVo@cpWn<0?f?gC-+X**y#O17IC+62h( zq>W3=d}h6NCFl9sI>igOXl~*s^shLTg9(Oty>oQFi1A7+n!O_CEl_V3g5?*AmDwOl zK_bl@GU+CKmPUnW5sIJ1n=v@3pP39&UfW5beNAgT>yaRNn14kgpK{QDcR=1`O3)9* zx5zbLc}-I?i3`?kFG((unvqI|L%XS^!159qXe3TPBW5%T3g&bx@ROz-ODRMFxP`5x z>_?~)`T7@mY7Ro!K50 z@e@;%_9E~=kfo`~%dlvgAq2G-PODwaXux0|oe~QAhzT-4lK?__IzZ?TNTcu*Y=QBg zk_1GgG)I|0txkbOGe;DX3Rsrrt3v)K3OR)(0VeW;5GvEEj`-R?^$ZuBawvAF;7 zmR-83uA6wpZczKq$FjzO2?neVXQ9FE0cdF%PB$^+y!&KAGvPz))8ar61`#?3F4kwC zE)xD_^+{^h{g%3!>fIZ*@|HR-@E<`=rChRHcDuBs13qnZTV34_z5B))+78gf+v=e1 z@XRq}j~a_k*d6{W-i&d@aT#r*C1chr@Pu>Oko9sWxX092yLg`_9k z?Rf=^LLPz3f?T%rjym4|glKK$*JNb`^H;RXWnb>7gUq9NRYy>xydKjHEL9)C1t^@; zqs)z~)?TRL zXw|^(pyA5zoKYiB*;|#3A*4{NF5w^wS{2G{chwC&zvJX@MJwd_n{rvrd+N@*P4k4* z-+ixU^Y5wS%*RFhYrk}~e*=!8D{s7}qH&z)JJ|xy%Ly-y@J<$<^JXY`a*m_C?u0iU z=9qJj7X0_nh6)%sWieg99-Jw|uGr|Xo4owx8#esDx%1-gOCu~_)ZVh z#yHGH;1A-gD$Wt4eoOw~5^g@O(!u2BaL_QOHvWhBog=URNi1(Nump4n<-PQ4E?fIR z9pZZF_gr@3fqG!gMeE`7osZAPAq&1#F!XPcy~ae`dMQXJ7Z6`AGFApu4x0Fu4S1+7 z?fMhm7@q!6ZE|gvoy(3tRF`b}&_1lr`US1FdNk`qLN(nzPKA=M8}OKX-IiEF?ZB5% zskeB^ljneR>|WqLhKFw_HLLJQ-L~{m!HD_Kgno+Yi>?(f_m7Thw(yZU$ODrR^x#@4 zAj@MNyW2r37iJwoY2AbIWX=Vwb$WD5^d_TDv!(Tm(EfN*SnA2vY&10fWfsc(N(g-Ukt*b0J-58=#bTPr7qTl zLLnQv!!K;cN-J0P`UWu*eFu!#EZD!Iv{DeO_r{ZqHaFP&*$~7De&Z)cu8QaLJ)fu} z!r@f1PU{MsG43n%?#mK&SilAaeB&`y zE5Jz!xvce5b&&rzT#QpesN4mPH0f`d^|@@)Q+37iwG~E@rc9R%@{x3mAb&{)7<=qZ z30~>z;8e*|wXf^E3vb!ur|RM61A>^$M~;qNBOV=>^*eF?pEcr7{ z=m9+X+gld=TwSupMI~V|u<;faTnAIzT1w;Ph#9&0P4s`APOw}_wAe(eN7d4@A7|XYS1r_ zws1*TK1z}%|EnJ`T3VoV33MihbWo`2(3!dH$`Q{hC8q2XJl^)ec?2;u#U%W6XKgq; z)I%y+_TT`~Los!cUE$t0NqerJb`4srKQPfN*78|?v2borOb7QbWL10O*u;@mtbb3b zmMUo>ThddiSnD^~Lt(p4{KN^If{R(gm}UUf((*m%YhyR2%7*WO8FPC{bprp{BMAI- z&zZnsajZ=*sYak#fo?7YEfdEU_maXqZ}#JKcL4cvnPu!$FR5b9e#aa<3u_6$Z$EO9 z5)T2Fo3|;zNVH6J>=6BeqZqV96tHc#pyl3OheCF2ten%ZBul}bd3||}Dg9A!{iSSM zvJ_pR1iqBySWTwk*h?uV2IV;Xoo$Vn)Em3GwO}*_w}wsQq?OCGbE`W*|2Mb#Ob54o zSxRpyG;magAoI^V&c*vT;w&p2p+Gwqg8m(gUVwLJy;#*Z>HyCfeRw-mV4!?@F$?V@ zm8cPR#7xDiNww{_lUf=W|0Ol@M>edVRFP%&l^T1_Pv*sYg9q}>h0MDjmQo`RISPIp4?yQB zf(Hc!Hv`6h3vM%w3LakTP%mC)9Z)W77GkrUe12PN*c)X>SG!X%srF5WZf$75iuadd z%-eP-dUwN_f$bZ>#QgvdhE!^nf^x7BN>xIMrsX=I3{_AT6+-bO6c4481O?^$LMVo( zCg2<<9Bv!wUE}!4&uDrSPqqbRyVqrJ`%97DlTyJIcuB(L*XZlADg$uRFJ=mBH9!h1 zR(ZQ<2u|>&Ag+3Fjm;P!RWEjVn@Asfon-^xyhVv8U8e@4Cl-&lcu^b9D1cf7z_XyR z=;YMUN>y#S+L2to8C;hL6EE|nNwhe&8w|Ip@JsnRY!I@E9@g5-r6}))ba>})KqdMq zd&4lGyPFUME3!?eu&O(xP&RL%WDGpBolEn7JTSK4I61QWJoa#)RHebot)exd*GQC) z_&Us-dLUsCCm|J*=ML_11uqXb!tHoN>#??jq*lc$DX=+L*}6ee#n6LWM47#S>sXju ztE47eWp4*bRVv+4;$BWArqxBw-G~iOkm@i*EcBhajcbdxZ&}(6H-mAnohAQZ%Nv~T z_m`a|djPWUmV+b4#tKIcn{(Nj1|Fg8%MG_WfpD1N#2xQOf$>Nq!pV^*!$WQGUOlKm zM@Rr00efhoet=I?am6AP5xKqs!F3B4@Sao(H>C*@1_&0F*VoXp2k+I<{@R@=1Ix67 z@MtnkTCqO+7T^A;d&@1h*hayf1()8iM@_V)J?P;g?ES^zX=9v!J2yn~(`{04S^?)! zp1O+nNvm+K{{&HJm$fsiI#gZN_FuK``=x+CjRRL*0YY3o@BlftW^+f7!ULr7K-W;-FMtL3tE)R3T^F7%L2PNA^Du#uwiOGoj> zciu!o%R=LS1_)O)F~k-{tXMrtZ_3@JQ2E!fxy<9CTb01of|jdi$WV;JdtI~)+@fkO zYyZ%#f?0tw1)9X6?o~->Pq@&U@&zsk?Lr&Q#15R7#-zF=wD+&5s+93)h3r{=bppV9^=#g7^Nd=bd z1iMg$D)2M5Wv{-Ie8T3zg+>E{d9gY2Rm@RHx?5L@_641O!>X2bt5JTPl5p*mL+@Lm zC_?V2zz&>x!+Jk<>uZEX7Sh~2lD8&ey}dO{0CH+gP;I3(t41kyHAr@=)3G(t+ChW0LZC^!8|wk_3k^tuYf;A%bJ~Z@M~{W zF1xwL!(hJghX6`Wz|<&G6n7e59Y(1|D^B7`PN^}wrXX<|rXn^>^f^~%D`jQ=Lluc7 zuNo-J)(T7c#I_L%!qNhSX4tzRjA9B#D+OZ`$9Tl`_H`v7SQXzTNE<0^= zD_P_u(~Xs4RLkR7%dt4K`gjmqJy!Z%6%)@IjFSS(4#2_F1LH{N>JKL#8h@vgs4(f6 zjLq1Xn=fBH%f26nD+kdh*^+UR$uJofTPUnGs$mWIbX$gT-mm4?9BJmYbOXyDhx@dv zQ&_Ho1g=j*&g6yE@o6kAE* zfu#{H^_uzX9Yl1h4y@xtIT1w)h}Z>;1j|r^S4$lXAD{opM@O)a-%G9i_bwF#JXyw0sF?MCV`FcPpD z+!F6U(2DT;;kj(YRkc66H6FK7UoH`~hC0AK zuQ}nZ15XReOI7f;D0uaSeuNPsKHlR%?5rS8FI3>ia=-C1cX3Q@wp}Tirtio?J6IDN;BLvEzI6ZrLl(` z3br%MS=(DVYx{U|t*w56M8$zgpyD`nJNsrL*caqP%H@i{ito>PFu8z~TEHmieMQmz znhk=Nr3Edq^8^D%W;$qjSjL%_cxPJHkqeIx9Dp(Mi;1A+o5E)(ObM{JcDA??E#v5H z28PRpcD4)zhM;9YU;@!Hc3>{+?5=4YFpLRW79S*9gfaInlgkdcYXVzbg7%>10bqt) zf@-#62oQ^MR@3(@9JSV}0E{(#$r*buI{QLExxM4vJmewso+<_FqM>SZG;4ainl&q? zF}ivkdc(}cH1)!|&J}dbIN;nnKbHnqFoDpvvBm|@wo@fOx*%|MDgm>$cofa}=T7hSlYda&ZC&WiI^cGmC zJ97kqUcj@j{iYGxHSWiy(x5~IW+-^_J_lZR!kgv5J4okK;PP+DT=^2Y z^3f65DZprnQxb)4SXleAsy|9qLjL?swB^EHXJ$7IA-OkW%X&yBC zCR#MZz``f)Pc(p}HFY z@;X*uA^WwyyJqtY?5}pzHgP={u4Z{1wKctJ&QNpkQ396%xIrgvCD*WT-?Cwyv>hs5 z8}aY;!?V8#@?G{glmBgSE_>cd8=-r@4b|zrqHb?*xvews%em&VNfi`d=ilE3RQoW@zuSv%S{3R4a6;Vx!Uyv6d+F}bX4 z7wuSA6(q8_i?)lg=t74?j)tLBBi;P5nqJIBRzA2GfWaR`wx7JO-iy1Tf$e9B=A`X% zScAMktU!Aiu;4PitF}bM0`keNeyu=Swglv|&E2$t z)kc$xZuN`ATu9!H$qA<-6yknB;s!MyKTLBH<$!5w_Mw|Ly7K>EdIdf1T{%66=h*4V z1BleLW%@vPOK!5ZM=tBsU0W%n47$Jod|Ru{PG?e%_zM+X{?@G|+t*ziT(>_t^;W+i zFi2keExK7hZY^U-1Ns@FLEK}7xbSb%oQP|g#ztDDu&O6uiUn=?ojGk8zuDE`1wb%? zitEHp>L(nP>DNQss^RN&L0;Gnl9})x=lT`0h>Ow3VGEOKLSXgt1@E}+dn_**iBo4i zw56*)gIN`Ihl#qcj7vs;v&5mheIG z380quSi(wDuu_5L@H}4^$BGZVd1f=yPy@UO1+TM$Cv^F5;Mp#I$v1Nxje=t=RMzT( zvUDg5Mb(5!=WaDF35Uzkb0Bl#X6W+OOF7o&W5D%Drb_YiB*p6x-OV=a2Ahggd~+d-^FANWz!-E*^8iypO!ef$+`(F2uqx6NWL zI!1}#*+euttwr}z0NDh<8{xlVvR$wieR%HwXDvEm1O(u;7QGg>T=aq4Z8<+XThT6V zFi!*IycV4hL=ep1FLe-W(b_p!i|+Lk=fk%=FkTNNoE&-n5Y}N1)}m9ViB@je=+JG0 z9whv0ExNOUH{A*Ev!_^#-j^oIFl=NcewQj$Xr;toY#@$cg@GRut*xY9p;Swt?J<;g z?qJCfnxX)f0buW5zMdd~FG`Q!$abJmRsCV?-tSV|sN?YXFdJw&?YaKv?s$m>x10;! zsRx%&45f+rvv}*UB68Fa)_blL9x)eb$}?*fu`rT$T|aFA4$a_>cy!V(2=P7)g_&6o8yoQF{0h08+?n zlgl!fs0~3uQv`n!)=>k+uK0c(N@1SgYr+C{)7*kGf%2hEoZs?P{y&AoV*wC8N{Ms`#GWb9qXpp~PU5Had zJB|?;BTlCBN?xY2s|%zG6^ob!)m2wJlk|rcg^LV+D|iNvlvGxEp;RSq^(2wu@+#*H z2~HW}MFxKa3E8pAa!VcXn~zCne5x~GWvU3E*=-3Zfwz& z`7Dz9`=-H%jjvPEFzO(kbOA(jz6+bVNHVFa&tm^9lG-)D^>bc+eNX+gJ6S>l`gQGx9t8D3izXOq z%TF=4Pee3{($IJI8D?`%y_U}bLaX7O=^LdOQT#%APP}mogm{aNK8z}F;7B`oCM4i% zrLX0AlUcc?QstV17CKn*NdhZsHRG&Erf$f)Th^j~vBHRREqtG8-BPJqrOa_o4Z8J% z<2*fY(Cc0_ykS`$`uDCA(=NkfSy#Se<(5g6JnGJ%htcpX?K`^v*oc4D?QK z#elhDoM83&We!$1RRN(b#7HCRWCdZX6GAv4cmg5O8VB~_AYyMaMp<+@)g7h77^U69 zDk$iw+mV=z85=z$5r=yRk)Ge8A6kxaTP!CoXZMyv1OMv4v@7to%I=Y@$qFgX*9UvC znBcj|Nd08X4tFjSFmV&7aa^E*IbtnfX^ zRDki5Z(wX}#O|(?qE+c*n9nMyOycx5xVdJHDuO_sfK4wFJEZo6-qIS1IOzf;K8jx8 z_3H*KXh1*O4QQ<(rzBV>;l$+=xxsjrx=I>cqTLa6L=1*>4D?5Ab<;3m9jB7JHD<1> z@dQlc#jM_HYz(WnvaYM8yzn1KL6G{tvK`+;=bA{pEEFwY2rZT$v9W8UA52~8cmK^F z@@?J0c7Z`!3$$JYYEGcoe}mKns*xewTtwNf2bix}p!4pdcN zz*?-6f-Ah+_#r>B&O?3L2KaSZO>j;(9crD4#R@XHxE?n7AZ_2vryH`_$mi)#I+;Gy z5RLx*_gHpeoz%$O(f0$0n_w7ppf90zxItFth#+OFJql7*jsYny_5=a{GQ>Mt|0D$er+_Q6wm>RY4^oQAyX|TAmdj(2pSHI{<4s&4*u#yIq3m}B zctj(yy(-5eJ>hI``#)A`lN9`o@jnE;!M>e?AktQQ+uiMjlBRSo5F@|b_S_x?%a?x# zme;o@2u(S4o^3^azFUx7m~X?+usfTiQ1b+P1`Z~Y>+)Sm(qOCc>$fO#cOfyZ!09_D z)>4IqOa6$xJx5C{PEz!;*L7ez=^&{j&Ik~5NY?V}b8O~j5PK2z6)y6((?ZB=3sI8E zO&VpVJ}A%pPqyZTC~+2YGH?qE*;P)orxggf81f6O&ZrVn6{R*Xsx0TRBvry$ycN?ebqy`zqqB&y=_`}IFnBq>!)&16Nl!H4L2h8ea=6;)AZ zSetE9K%g(>Ai|%figHF$1z%6IR6qwjg!DMn_5~{lP6#Ip0QCw3wif`dVg9Lh}f_Ll{1t4it#jI^aeb^KcHF=3iDL&4SyQOAO?bhgLi= z{_~!|Idc0JaLo4NWu@4Ibb-RQ1wYA&Yu{gZd1+RMmzVeAP5#r>1g)t+2RK2m0cslv zeX5F!mA1QZ=HyyG(;cr=lX4?SO-bvF+hze-q1mcS<`w`@+4Wk4|8X zN8YuQUEV3x@;!Z#Zc_7a;eH1S;ZH4k$-@4^*7DeIShK&RQpEyd$bozzxBiBW_zN?O zE!EkkzoegQrB1bTs3XoAfYviZ?F%mt(+WR$pPTCnN0DwMw5Y}(ybl=CN3?q33~II5 zfY(2J!z+O(k|2=(^uiUY;daOz&pZjsaZU?Bo*_W7ZY=N5O%ff}v9A=+F zkEtvO98y3aK+YGk&lM0j1P4?0z1D>Xr1usS?zK25tnyS)*uJ#_G&=#`JRu6{t|car zSvX$#FABf(;S@#?h3}h!AWX0WVYkKbG_4wQ-whvnL@!olw^Y~IZnB-bkvMe#?za7g zh^P5~#*{*Lw2$K&A3sAC8qJpMmZC!IBn!$1Pbd3@S7|v(ICOKx28Yr9!SfHj1Xd)l z>}y$zZLG{5Da3P4Z%(Q|N|skNVTpUB9M$px%(Pc3ZwkbIEt8i z(Kq=m>%9H$Spj|APRrhBIf?qJ=MRWc>aqM4R2M}2%Up28?8M$Y$I9%N5-Z~t00g>^ ze@L5V6b)&A&l)}sH#%ts5=byFh>@nRW&5&)`=y{DEpX_00x(|zp$S^~RUdY0zf`%_ zp$T@`GMqGkY%}}Y6|>x=!ac|M!rpUcRunyFLpLF}8&e&6ne=w6ocKCTczP_rr6}f%sK`#8H#HIkO_`irK|D$OPnFO zkbFGw<~;K`C?!O`q)ka7%|8vPq@1er$>|xYv$PPtnRf{$a}xNj?$rpoSJ2Qn=R8SVwB1$E$q~; z8;7KWMN}iZu*FBDxQ0Xcju;gRUlEKDg=$Xlrh$_a3@Ok`g2^xGp%`8RM@4C#r&#Dw zscFMUyQw2nA<(meLQ;SI1zZvn8o{M@S%UQq(9puH?~YaqIS9)_tPecS9%V^oidowVLZW_P-r16$Dytrg z$d)R!s=(_|>a-f`p(Ds+IAcYcFvIG?SUWobE9pO2w|28%u1W#8+5q#-J+{5O+48?7 zpJHRMQJ3xtHYKxjSEZ7T=@Uy9SNVJABIr=DJdjf07PRJHT*%QMM;@7Y^Rq~po@UrZ z(KE$a!{ca%)`xXGE|peY-pPJBF8R38<970X9tjNiW2b|kLeap~pLep% z4p(m%Jm!f{wuwk83YE`pW0U>?Tkh=?Y+1aGZT&|o8@dL(om`KmzxI~tf6T)(JVH5U zDA-N5G53>TZqiOx`6NoIrlddL%6gv!b4O(sW-ey#E^@EKcIHx)m^<+(+j>%}+v=Ac z4o1TDS%HJG@=dV9PJ&?WkiuBq32WDXu$t`P%w6*@IHPk$ zo~9Hq@<)j?knBP{_%q;8rgnhxTOKc9VD1iL?hh1I`n>W$)()PDGA9vp6SfFWbMz`o zghTwcusWx~u=3jlVfQw(p{K#f=a3DhpV=l9Va8@f5xhvngH^w8V>W`16>wFx&8*}Z z7p$8aFu0xHLj{K|22~Exn+$`68F9{RZ~& z96H1GJ%u|1-N%r#_SriFMX579IKY~pN1v_MiVZyvk-pjFXa|Y3u~S?K>YP)GBGTE! z;~=MWfw_7UyL?^>ADsFh$Z$%5Natz`^nad{{?l)HhJZl^G-s95DQagh;21=ePar}6 zmr(}WIw5|Jenv!^amdbfqB@f(l8gTt6`EL3$6kuI6j2iYbHq{gK1yzikAhtNAH?55 zsez}~6Op<+XBn!&PMux+5)%I{u+3O(6XGwmQHc2}w(_``Q=P_@2OHRe3($qE4MImI zuVtq$z);SFPW-#bikJ_JB8My3k!xA_MX6@P*eArZe|t~qy9#Rt_nyY=wrhsndz!PC zEx0I6F~+nmmDI6QnN)gK_Inf#qg%1nNk58v!E|J+#k5(kEmul2Z|`o8u_#AIxr!&~c0hhcfMy!sbf zpa{LFG;3UO-Yoz5jYP9feB)@A=_H$cSqhKssla}Af~m0h6r0f$s$}2DLvv4wv!Pyb zsMTtmbb;!~k=-KL!)X#DRP zZ2lFghw8^QyhOJPRAT*=toT)_YO6YYkGR08YYxfE)A%N>vs33&7)wo9z5avsd^HV$ zWmJE!7AiaKPj=#}R54__GbSJX#!w0xTiGR#d0&G;4|A~_^qoXqS)|v$27~_U58k8k zQEo}2x-24YgQ&0XN8}ofT&qMUn(>Fyi6Wdj(eJC6jgZC)BvrLPSjp=!w5C-o_Bzr_ zE9uu(uu<3X3f88Tf<+IAh<+>Blk0f4_nQa!^0w4mRca+mz9IRlrf*?IZc0AB-)*yV z8ra;F{<4EDz9EIH_S|PjDg5nEk>kr|kwf14?;J&Uu!x&d$=XTvg+jgTM}l@bk}^U8 zTqc0k7#)xPR&XTcd?K50Q)*kgrh+s8NcLWK4X4Rz{p(&mQF3!Cz!G6w{}@kb@YTX` zf3Pxlab>geG8TFZUy5if;vpZFDx92yc3~aXhRsnWi>HEe)Q+-2K|%LcP_C{JrOsN) z4pOOqig-v@dzP6>7OWU3Sw<)*;dYcX1*KU5N?oPYmrGdB+bC6|WZAn!p>>p!WxLH@ z>e=O-kdPnkDD=)WHK$ww%0hvnYO{oWp;8AZSxVZolvJ`*cxErPs)F)(u~M2!@rjGsIg0-(;vw<&BHAlOe0^dsqKA^j2b-#QktITj-&({{ zDE?-tXxrLF3Kelm5mG_h4k#$2>?rjWlqm(ZSOkiy;UcELhf@C^UEcv0-NjOZfNqP}T1(=dbQL zmEl|Np?)r0!vDPIRMB%a)R#96Q`6eqa*F>E5K>J7*TW-b3Bd7`wPGqc#eX|NdoJe{ z?mKySlp!ddwJsEE5NO%25Hw^tRZNe8_ZI{DOsu{~Ht%e<-#&JjQKAzz;RR)As+H#W zHgtgrmuEW<9MjDAPMj_hbH{YB(@n-Wgc#~l0K}v6xh2#L?8P{H-^siF9HK?Ng@$M3%2vYD3)4*AH!V7(d%o45mkMj? zEj)3HpsIFMFKICcio8Q8-|*-wPGy^kZ%@LgDBrI3i88nG?QKP4IE*$33r#iw$f}J$ zAXva`eT!nUF&tW3%DJS6MNa2~e9J>8_iC4KfnSq;{vC3{+aao2rW{xwfen&HrQR3~ogZ+RA99o#d zork{y^I&xuq%K&*)yGc3&O<4Xmb!>muSMKXb@B+0C7R$3+7#ymiv3S|zSi_y7xCdL zcF^5l$d{<-b!IQ*`&6fzC5_%xOqId7u$pHy(3LYXTsPJPKKk{{CUbu$RLbhomCU_d z7D&OAOiy5XXqegNg${bq22UODLQOHTFa2oBDts0;VbT`7yY!Y?<(%2Csj1(e7yl`LKs*Hr{ z3twcE@I^`(>nD7yj>Jr{##C}Z1@G1DB4TNg3uU}&HH~y19cxFSxk_(1^vh9Jf&;pZ zZ5dn{a!?D|N3OuRp@m$9OTDEW<)aqz4lZ?ra}*DxLv-|Tsqca-=CqJna51CMjc&~A zfOO$7!5ga%2;^Of<7mmulIBS@e;|DPe({jndy>%y%U55m23tR_yxDR37qYuP#WxVH8 zlq6fmr#yA?Ha-b021$C5w17TzGSf8aH{csIvmh#Ho&onF7A`IP-b~>XxxEM z+{VW~JBoZ;0EY_r)Yeq!!&kt2kV-O`Z$Kbu@|eLiO~{MyD=6+=o6^-&!%jf<85+tR z1jkD)9R+=g1+YS#Xm8D(%q)#kMK{YUhy(OnCE%nfTgWjfTYHiAls(edWRE>D@Clf> zEsOnJmAZaPtOQ=_iN#OnVW)7%{SykwRo20wA8v`1JQjj7oOTpwac``kXQ zpyBxO*tMk}f;P+f&n(N*i#qXBX6n=9T_4*;ufYb zS{ic1$pW?GzhuG0t0LWiV}K3b(_;e9$am87elMK#we+B*A(R6`wDZA4Z}gm}*yMeW zrCp7hn*XT?3+GQZzHkb0+i3~fTL>sw_Z}7boW{0#_oq4Lh)EW#)I#o=kDIJpSc2LV z@;3QSRat3Ekb5EjG2bbS{q?aZ#j9WV(R`=MY^5b=%@_V5-zk)RV+k7kh1YrM6v#p? zLE&Hcke5y&?CpmlTAx1iUnyw6CFsy+e)FYMcklkt1dfE@WpCOHhzw|d8AZpnrGxry z4c_6EQz_gekwUdBGpyi*8#qPm4 zv_PcOa@0rwD9}%Bk3ibTh`=B%Fi34A0!6l!K_G1$uygjtCdOj0`8rz%n;9`4>Rz17 zqV5-B=6~W_UOTz5ccm!>5%1I|e&IC+UNsZ=zptIf_Nq!cHnnsh=3r~${X=2iyvxh@eUIcFjjpq-l~LKJ)RJ6BC5mH7OL{JMe~O!4nns7Gg%IXFuZN1TKej@w@_Uoh>Q}NdS}Ru8BEnlO+_dv!k{j3Ez-^?qJxp`3dj0-Ml4+D60}b`yvUq12=RVWK@6QxcP}G?UmO zuT)@1kv}b*fgZ5y5H{(Pm?=Tbn3Q0F5oz3HM{I0&fwl~$y`(fm_yK`}JoINy;tLMB zlcb~r$OWuXR!rOh$@{w`%Q?zXWQmR<5-&Fq4D9dB*jkZ#31Z*bA^+(IwM5CJCrh-e zGV3Cdnz$z#1#tRGiSd(0$T02jIfMZ51|@X|^vVETkW0?$lcR)EWQ2ymrEq*6)=7jM zR2IMMsfCWh^we5MikJy-Mt3|s8-U<7*VOo9`P)Kl; zBRi3S$~43W?V}pfHK`Ij3rqnhN5GsTYS(lZbTELqNyDVNqIJS=0cjpuIj}Pv4v-e; zP(#cm0qjkJ*uj#$&G-a_GU`T-sEZ_94uyhA)K|q3EYUX=g5pxVp#Xj~j!2 zACb~NN(mvm;SLrdSGADKa7T%dcUlN>H31<&9}SYE?;NECzI5e~%FlQ;1nQ3m68PGQ zq|wzrqx|{V$86zVTSaZN~K$++Z({&*_VUNTZ8P@87L z&9~it!)(yiN7Ms|+>Yxe;?i(sy*j@9h}e^x$t5&G&cq1*sQq^l`MP)Zvvm|9|%5eaqL0z>5ksY{MXD) zx9qXxbc?&>^mb!wPTQ`My8A|h{`?hGS;hD0m~XS4l>|ro;HV`qyl9pBHqkLIxsipT z`{)zNBgCf2GU#onbMFg=HdQt=WLrrn?#D-xokebvo#TzH*=ev+veQa~PWcL&zk;8& zW4@(U;Qj&Rstzzyy}%_e=pI+!=y7D;ET=-A?s zx_n|W_D#35*{C(Ccv~&0P_(CnCxywd>MJ$F{ft-!{`#mNSNU2 zQi7ol=lv=G6u!qCFUcxiRZF}@6t6#Ebj0kUC43r%$BXcsyhE4|mV>-G^bXPo|?wqn6-2{5<6Mb?Qwbdcs*Pc~x%qTm8 zP+sLI5L~GfU&~X{7YN@JSryHPxsY^UgF8s{KR^c zKmTISYC6X^q;k^KCe9ZBlbiCWODxcBg{v$?XX}ynSRFuG!yQCs zEJ!Rq=*u0XMT!^?iZpUYj32rFGh!;J72Hu&fftHGKlTG5<*Uq{Z!5u;=n@|A){e|a z7xsV;cVxc3U1135Xpt`LE!mA?ag(f#h-?| zQR<27q7F=7WrvcD09g?_gz+bitcu=n8-+m>dt$2#dM%1lfd`jlb#y-Uc;Aw2N~wcR zLdBWopklmT^sFQ+=hz9^O}N$lxQi2;>Nbwa>{}a7dXbk*l;eE06RYf2PlFAyg56xe z?>aH}a<%Kw_WUS})684*(>LZO;BFU?qWs@2-_rfD zDGYOqr27eovJE#Kh~aN6M&Y&pcf992WV}n}csK^wRmKxXZ|SV(hoE;bBHv`Uy=v_2|TWmtNvD*+d4B(*SRu$pSZOkS4|ed?p124gC$_J2I%^~(=7c5 zALa~^0wi`R4cm>p>17}eWP#Q%yNgVe@_4B40>1`xfKlxv*hRepu(f<3qaJzwR#Zo# z79`P0Fb8!L(c)uVSQ+l=g4T3OCm!a)!W|d537Tr7Kt9TaRnxhMaBsfXg?TqC4xoBI z;J?Y-e1&9S0I)5YQaXsM2GkAz!GjUXSNm<_-mYv{`5}bU|Mpw+3)D%XK${}lQh5Ar z{@RsQYFC*&6$xn&s@(RE)#>Jrs51rURA5aO3TC+$i!UK+W9iEQbmbTvHn6rKsKyq(mLpXMjskm2*L@F#9;V)uk)BBefX)X6GR8itix^I3B#-6dC%;KKRl1pQx#1P{P| ziwu_a=miO4N$@tK1if%?C(`q^{hNPuM{-e1MREyn)M4mvR06MmM-d8oyniR6}*v`WrJJ%b%xl$=(5GE2(p^ZN-=eR^M_>NBZsIjV#veHA!r zGqnGCp-3--`jO)9^Ol8*xDjenWg=$2%j5YBejatu!%c%W`wF^y1~2Z#e2sU$$hak~{MM8gQ$oB)S6Y2;#@Ma;t`TC%{_nc7(c7pkmpH2+q*(b`zdPuo_`)5`AM~NC>Zw zhL`abJsn|o6POyk2!S`qpbiEiwLElV^Nk;X)&UjQ!eOL~MlebvI9OnACrmqm34%q% z<^I1?>@r`ap85l?=fixAziZH2{?@W4fRrew;wgx{{-|E+X2c(8xq~M|n-pK;X-~6J z$}(pv`pPW48nl}hK{4??5#KNx?Qe_q$(itBdZHs7ECEK@M0aYHc7|W_VRdyG=Xfb! z=3lbkC&{YLzu@Pd&)jt$m3Sv#*4FX)M;UIa#JBmf7{`^GKdKV{;>&!T!(iNK=oVp< zxLiGQpI0f*%9T_rQRh$eTY2MorRuRcB-5n#P^`LW4xeA1eWR;Ahkq>3y6LWD@J@cL zwQfcRUrTO827l|vn(Ojs^Trj}K=zL6tz5x)c8YJWz-rXJPUF_}F+teILw8Q-H?5)m zwsvLxL5yw;=?&CBB@+7Mlf_C*@w?okA_k!;UwHkB>?g-_Z>3Se}nmZOo~VSCSB8;V^y<)uCW)d5y*V~nrq>Yy@(DPcX7*9m==28iw_TE z!*nq>`1L?mv%(gv9YI6tIdA~w^r3}w(|-SRnvya;@amPBuYMHJ2(E=wN2=!s-nTM~ zYgt;0dIwQRaT8P#Y zU7{g$k-3sf2$iq#+#q&PH~t;}F_={@-{=`l_T{Q|gnOIri|PJSJ_(DPW1jQV!OY#$ zp%qE|tfGG!F6bd0 z%E+{$>Hb!hqQ@^}ydT|3(z5(y73Lp%ARak`=(z(=aPJHH-5V+tI&LyzMxQ8qlHufuaGP>Uvsz^J}`Vk z6^*QSRq{%QH)yz*03=8p;+I5Li^g=j9`okaSPk8y1AIa?HZJrGMt`DTqORUU*&lfm z8+qg$L@j4}RTTFj79C|)SMKM&A*^3Z@28^aKKcbscOU8(Vutj+q7f?GEdo4>2#o-! zhz2ZWHzUm?Q9t{ieFfq;v`^V*Qr6hA)FjB6i``@CC@{Kb{Lp5*@@=m?l_(BYD3gIw zbSS#b6AaG3KDPbrGw~U|Z7TRo5?xBV#DaFksICY2lnI+%IWX#GYM( z|LqV?UO8rfj*2e!#l60}_wwR3ST*kpPb5dWGKfGo9Z^FzHj%iNmyLPM(J&5`CJb{Q zrdA?Pufe=auK5=!B-xu4rYb--hr5yRQ^N?r`fL;CH_`5E1`p%$6z2x5!D|{IeR%t;KE`H||AxxaA46S}Wuyt?SQ|pR}%@CO^fr7e1qNng!V)0xj&1fIln?o+#|2 zh)!!3q4tm%2Ew4F{dgIfmiD9NXCHjlkyxXEctY(!|J@Fd~X9*&e`Eu5rruA`U8Hy0hmv| z&r3FBjdk01^Z177kGUP^XB%VYGP5CTW7n&A0e6jLA-We;cvK_{DF1G=R-w?S#iiTF znI_w#1+f$Rs6PQYC6d*&%fZKE^!N<8M=H-6=h*>3Q z`|qG4kzQtIaa}YE*3DVZL!zOQZC50ZcWo>@cI?N8M6>D@j;<%8lZ)F?op?eIQ^e0%+rwxHQjCJ$s(lXu@jgKCa{ao3J?DmUVnf6R0S49lzQH z%~#)b{Bsl5-~Lk;YEP$|d~ghl(b-nvzs0bgj_uY;R;xVWWn)=wyV^Ddyk#tArH5AL z$+2v>Zs|4tI+j)UnM`#7?UNrS|Ip^CJ@%6_#OAfW8+c?>B-nX1AJml92paL1)YtNV zUl&!h(!EGJwA7eZ#N~Oai)yo)A8d+M2QLk~rNEZ$dS*{v*pzvA24E}}m)=<3o0n*3 zu#3Po7K&PZH4kluMb6q^c#CGN!Jv1aUxT<^oE;WAFas86id9ez#Aqb^6Pkcro9ZJ* zeIIS=Wt$U)u1j)Ln__CE{c@qDCA#miX$5)ope%;)nDib}NhB&SwzDMq-LEpy37>iK z=B$CP!Dk-R98$aH^5M-{EBkxWod9jc@ApEG4QE%_NV^QaKld7#gJb1$D}VDY*dC;3?e>+3ZWx;EG?6-Q}F zt|$z*Fvpgww%^ZN#$n`8VK*Nh2VN?#g1DA9tk;`zZRUw77KW zUNP7UH*A5UtAdYT=5OLyWX~$wsj%HrOJAn^+Tm|Q!v@ritzevnN8chm?;}cj)365( zbB?j9W_d9%W@T4A*q^JOpO-GFD0^4TqzZ|N;7Kw1l=>bBx2 z&ujta=YQaNEm*w1`3ISswpiP90eKkE*uQv-mMmSz-|@d&qG>vi%cJ9A9S<$z%i>u* z-P(8jN<8z|W$fTz;+ePZm!;gh6$>zCpOShg_{UNb2X#X7xO2l$uZO>bb1H2`L5&=Z z8Yw5=($Ss8a>df#usU9gfQx~ly!OTfoL0D!l*$;Yq3>;2H?dYEE+cVJeQ?7uimJH# zCHaVpI^4D#Khz54v|v8J-ip<5Y`0WaHpe@>Y-<)<&i}2D)PQm;j@pYD{^SaY`nC!{pbUz0SCL{r+g1Mk^}1-nhvV7~)qUe~MGfiG>tJRAIS zL-c|+!BMMjrF^A3BvF?!9+1yasSnN}Tkqj$8%?x3P?by0)u|&E^OtSV7`6I`hb6E8 zU7c;bO9Cc4n=j(y6IgY(-p3_FvtJQIay81XJ>QqWx;r+5>c|HA-sFC5Sy1V+q)Mw? zFP6!>wPjU1bVl_zw;exr2ZzPyTkCuRz*aghasB`3++nWJIlZWodJjV*6qb%|{Aybk z(j*XES#~Y%6(=i9%F0x+TQMN%m?E}?Pk@V45%vB+xi{%`J1~cX-7!~?zx4-lLpv5| z9FYTzXy;+)5Vrfa4*lF0p(NR#c<&XPO=|opqZI9`F#(70&toIZL-SUfyhNhU+MU7l zRj|G!#c>{X8asFNZHT<{$Pjxv9FoSiq5^7LD~G+qPROX*2mtOy#6G$cM5Z0iAS05d zjHIfH&MKT}vE|ZydW1dP&MUNM9!5LDkjEUHgF`ND6Z_%MAP_8cCV_L6zYs{j9Ayo{ zX*13*aAiog7LoMHge}m}A z0dGKnuYd5nV4SjmIHrAFXR&pT(tVS_VCrvR%NpIg1uq$aw3+XGfj&u@FDI)OZZAG3&#ceb?IA; z78IOR06u97QNFc`(n%uKKosnI6*O~|0Jx;X?m0>`Ehq-Aro@aQB@WS)=rf0?q$yS4 zKy%p$Mk#$J9HvK1=&zT6Delraw|UpBPUx+>B^;6#Fe%SslK*MJPjq2b%hl~drgoAC z?Wcz2>)taj03kGK&kN_2ayLpaHn_h~KXn$U*opo^&6nR%^nG7`C(!rb@_Pb(pA_Hw zRtkl@Q1?skG0pp%^uE-*%ca+Y#x00DTY966nm+ zL@QQ$-)r7*>7^aK1PzqlB+ctCy|jgvpv@hmf4%1aK<#q+zGIsAiS%9}Z;=8oAyBM( zh)J$=qf1_PCqA?rdH^#z@wMGp*T4`FZPFD=p@RHY>02+q-_v)APCTGH>sM+`N1=69 z%!7C2^SiSyy2c%Oes|WyuBG~#`w!K1ahq^J=J5K5mWI{4Isc^xYhdh+T~6xWG*B^x z|BP?ZCC^qJj#G%ff=XLICs7ATZJI4M$kms z4_K&fM=ugoPd+Z9YNzHO=DmBeq3-Rlo=LrHY*uKG^l7i=p&dfGciBgKPuaQPYH7 zIOxBt&;K;XDrt$8A&Vj=>}Ys%Z?DwZ^!v~mx)|rEB&PF!UliAuoqg}Mn4-j_QE`w( zZ+CnQf7Oe5HvUPARfS>|O>ZY^p=Cv=Sbrtq#@a*OI9i-s#dfS7{+{+ct~aY#I&3h) z?ikT{6(zcp4dSY4O`ymV(%bz;3~+S zj$H-WG{iAAwKXIk#c?;g`1FQP)$b{5(uzIhr?#>3l@{-jvYxnWDIbku2m>7{gUP2{ zaeF>|pW>~z+xixrB%Ha(RgX~ynDnm{d%k?15)%H3?ygAh?w}r9P5O@Pi&jYViKvf1 zFO`VVD*sQSaYA3wq?CxaTd-eo^Bb2;5#-FXSm4J5PSM&+@GQJ4QxWeBM!p6UGs`Mv z-KXr9-2QgQ(qpU8q<~M|j|aCj*a$1wy=a;}?LV>AT;T4rkksMAt}M2jz)C+6L=m%VB$NBU`_EMGE(9U3qmiPjBW_b^F1^bdaEK{RvYwdY?2D1N z`T#?eyw3Q7m>G8VyzOFEHsBQ&P$B(yc2rct40g?MN)&BaI{rn4=KT(+dh0e%_=Z)t z`_k(zZ+pIEm0n>NWX__rkBURGw%EuD{VYbap=Iz9_4IUvi44w0uy_xO1{lISkDbxMzdUM%O-ITdEaR)cPZb)ee4L5_y+^g| z8n+$H%IMZl<8FgleUBa6B=--ll36fMkaudp2M=bAJ$hy94^>W>BDi~ngZFX_I0zVV zX+5#v(Ggo0kWJVk;Gm99D&QmfIaT+Zu~jm=`-(NQRq7Y;LVfYz#?3VFxUax!Jrr%N zdPVO(fegn8Tx_IdIxvAUSgjs-c{QJZ9>VJD^o#iDZ&^9s{#%T6Z%!jm9o>K*WneZ1 zJ|D~PQsB;+GO*TA=4~6v6Na){wk3GVP!?y``|sC0uBBeaZ5wPc(wGM%?YCh1Bf^3mSa5baj?^#%h^-?^BlKFNRR^)!m-{ zDq&t$u-PHJ#yB?Kt(OLyb<&~~T?pSl4ilnyq~gvv7Ux)olmf{=Me|zYS-9KMUnIR# zCoFa{sv4gYfo4jwyC(v}Yo?Rr&xiX{2V3 z=7r-~klW2wG9?CT<}905l}AosHQnZGu%pKI6vKR)bBmg1xE2k59l(jRC&g z^Lys++Ix5bJ;R5y8Y2BszP$DKtiIdil`_fA$1IBUt74Up5gNi^D})=t{NDE{a_xM0 zwTUdmHD=gXv^)4n+I?0?+8MbP+QoupCV6YHcq>>|5dSifwblLR#oJC|KCC*fKa$)Y z@v`R$YfE|X%~P14TifN5?z9{W-8Yr3cxDjf@S#g z>PD=lT`0qc8CiKJ*Fk2wkIQgwWcA!?Yq9ztwotrMNm6{dNK*8(g3YVMt0b`+w%NQ} z66@DLVTR78p`-JU4&nwJ{hKXzUtd~4T?&7VOov03!hEbEx*ppJT3mzH0n|L9xe-wD zV6-9q=RUUlRVu6Cmc39iQ;=n$GN>Z&JdM?Io2bDyTEToP@|DwAbmK}ItUX|6`neTI z4V%RHHoYl^68ps1)}%kvlx$J*C>qPAk9AR(FXw*KSr@mNTqeFh(~@{-1wLy!tK}1; z!Nyp@9{VAI+~oB-em4yZcW^4G!Ek9_XU)qmvPG$|< zd^A`qz|8RumFJ(6Ss%A`^Ciu{4_M;0EYHWJum)b8G}sS-nd5!*CBcXCe>au#AX*DM*o7+~i36Fk6X+9qz=P|BM!ILXaN z!)o;>VaajSA$R^corQZ{z=8OISeMsHSR7tWdMQ>EM!WM?Gf}ZpG=v>HEy)MD^93_m zb)6c_b7rz{b(4d6O%uxSw=*TN#XBsxrQG;d6B?d!8mxsC?4W+ZEY`VA>Lhy`dehiJ zZ9tP@CVlOCR0lCerY+X%0L7S_rcl%db8$1&dzuTvv9pPoA8`~B!tDf(=)ZK~eC;fB zi?Vz3>$6}q9MUCE5r0^O-&~fLp3Ua##(v-%W@Faq#AW_qHj62tn2xlb@OmEznZLzSaEKIan}Su$@2qi#5}=-p)hkvYPI{ z{3vyBVT&bSnzoq))Nj7X!!9#DcmA2#cNjO5bhBUXpTJoWUqr^{w5_nSY70`gX`cTm z9OrEsc0BAj$a+wMCTt|6Q#0tDjnh=$dWw18so_Qb?aw$Ip5jsTRLOQAKl4^R&>~ zB6ObrZZXeE(@%3TnlMj4k$)omy!t%WKd6f6*+yog@{1fsSytb+B^Au`&k%E))GK_D z!S~K%UgiI8W4_!BlQ=J-1yQIVJU-Q*y77PJp$1(}k<1l`eCE~MiKTe#eDsfJYp~4C z7FexPJbga%35-S0Cmy9Tg4S?N;Sa-5pJ*u;QpD5#TyUdi4drL%WAQboJ%2x+g#_G2 zK_s)_Ea*v-k<7q;+ENh_y!SZATP(n$bCZdD*aGGgR8Un+rKP%KmLN|>2~f9@4lt!c zYQ~hxN(ze{#qYWL>y%3YFIB>!>+XkGp5Kt z=wOEE5g!MLW?ACXr+Cn^t9pg9C#|Ump5;yC>-RV@{+Tm3?^n~LDk^NGGO@#dE9D;T zPsu1*z-ugI{*l`>jJq4i#K`r}jwL~?XfQv8Bp`!E_w$~h6Cdbkz)^b*;u#CEZ6$vh z&s@kV>jt`Tbs_UBn>ZP>gy}QIeazS_L`Cg1_g=&r`Xr8*CMX_m9G)b>yei%qhD?Xm z4`cY)MTq=(l8h{;)G4xZnr~RdB7B~XlaaIGYLT~Ukw=Z;UluW6w~RM*g{)L#-T*O= zr%ya3DsqYM#3H%Alk|F=1QfHwE9nptvYEC%A)7~a=2I545xPSs`SZoBl5XiqUU~_O z8Cn^$FT%Kv&?IO!)?AZopQKlCEc4E(U8shcBIluI%iMEpNqIw6CJSMdUHCjq<{A$k zk;#>F%Sa$XT|Ss+En)7CsmO7%il)WYw%y}HKIXtuJK7fQDO zRs$}!0+zBTL%5Dglx6laZH~xAlW@Wpx)3Y*YMF1qXAwKt;HVrr&Yv%3G37E7HSvFj z*ksU#jv=EL(&IR9v5W;r<-r0=Soy=W!AEt*reso@ z`$FD%IfgkG*GQ!WO_ECM@~cH@r;16X_0O9qfu2@CQ!!H7#;=v;Fk2|iNjTwuF{!k< zeZ=;-sx-TqzEotT)(XH@k}))e-<`#cE0|kET~Zp9=RDe6c0T76l^t#M0q5ROe8`-i z|6cMn`WJqB1&qzqRs7`&R;`315{5yUQn-Kbb5x3}|A3>=`ldG1XSCU~+_@rN0L+@7OrGlockhV?EVZgRM z!~U;}zpx17hj9`kSi=wlCbE7DZ76?fNgzHg&yaSFx}WCSfr_x92Ra0&Ve#YBcK9b~ z`O#1#c4Iju2lmCug+MJdM}*E3xrcr3P^f=4jul$BuTI%~@vp2|!^yC?va)O^iKz!h zwqXr|HQV4{g4B!aRY%jzk16uoQPiJ0s_I}ySL5tK#b&ac*PqK{S7Uhm?+QMBHFkjh zwnCJ5H{Dn%cgqeQRnJiQIaH*@zPJ?@?t}e6x7QT2{%qgozTwmQ`Hbs6Gqghu5-TU-wTs8}-Ie zS&eoKp$L(YvxtQtbxja2v5p10E~8-7vatf}Gcc1!tz(@_q}~sWyEyL3r@ zC6T{Yng6wpRq-wM5#*15E90*kEXbdjDap^R%)QsMz}`PmFv$0VD;FLU4i;5nx$ttd(DYFWY7eWVOb`8oqDVwVsyFi<#n0*lhfS2v4A8<2s;J!J;A zVg*WW*b=}9}lY1Y56%R{3okC z4pEIUYi*;x>Bf(5WWSWyhEu|jyZ%0&xCxmXK!u9hX z@7av9c94%_p8^7f9*Tc-);ylsMo z59Dq46dLEyuKqB9tQQA273#DRP~C=dZP2arFqyqS&ZF#=2Kmz)zOqlLK%XG@9?$+A+1_X#58c8do7TWe8`4C!t0LPQqY!CVT9gHd`f))3&}A7Z;)_VD zAE?-)B-{Z=b?U^oZ($*}n8etEZJ`+*dG)O}iHDzEvxZ7fvxzAn$%hBF|Z z8cv)Q&H?PLGnk4|XWPaQd_{Ej!G%rJ8N-f5B;p}ijIns7?X0fP#z8XmYqKpWw5HhT zq$~|ou+3cEh@KpD$EV~n52e~|b$1RU_K#NrlIrwMkVBU_Yy=0Z>*@L&;ffJ^sO38wsIHxS*-aA=Txu1JbCBW3kpq^5i#@&g-kkD;>+)h@x+`I;8j-2ze z=>AkJJLucsadWsngMQ$?$jEg zR2w4KPb3-0()-RNR*IfE?OdDyu=Iggm22x++~2MFH?rW2Pq)~Dr_cE3R;2RG>vIUPwP;-yP#Lg)oYtnywik+RN@SWs?so!ZJW|T&UdDJfS8d`7Bv{H)?-i4l` zzx0b-$1#~*thVPLJ}4Jox=AHn?qarrM}Ft`cCnCV9mbLAc?1Ek!+wy$U&6v7iOIhc zX_v69!gkm%61j7Ngua*1N{B|tHYm$#S9jiVHw$di7Szes2E(Q2RAI3wORwz&&+tT0 zS;lUl!J-p-sxtWmkqnX?9n)F6`GMWIyg+HoukL2mJ2mZu*X(HN3Y}Q$E2V~z@cOBx zsC$gTD`m#31e%RQ*OL|pQ?Qm`o{sYXKy5E#gva9>1c*02xx1^^d zdYb(om&XlzSxv`&?TE*ts$+A0Xg{mS*Y0IO9(A+`1AmkZOb`rQQTe02tc|X5ZC-aD zYumU(D@fr?3wkEKp)JY%RyIkK0HFCe?3Kqv92)=(NHLha=&9;&;7p+Uu0PM+$HH|P z7x|Zctf}K7Fb)&1ZtKij?PuTVPR9~I%{~6@A$hNsDp|oW5xQ9Zi2EI2VQnhaq>RG+ z3{`#9ZQPD&qb~T1>MW+qD3Hb!Yr_bN4TUrPLuo=H^|#h4DiNSlwzznuBp1ArN%%pa71~iI#^Z0n*3#QL*vEci44xaX;>VkOkXj^414g z>%gR+X<9&75kHzR>ht$|F{yM19@;AU3AO-j^WlT6pUV|8yy7WeDn%zc@YTHe*jyB|9C>4`s3lr$3zE;yeLO{{7{~l`K}xaY;K_!p9`= z23gF<)n6y=o>K+lcd7pj?s}N%b+>|fQWl!LTgiN77R{!#<7cy&Ti?h3f@!Ew%oyWn z6iyk`zE44P0Uixm9;-V(OBC91Oq!5$DcS1gHaO>E3O|$&bAY~Gs}6z@RDdySgb6UD zUUyC@;LQ%PpvE@CW#v3avvqCTYL&7LT)OgWK2<~9L1*JU>nBkoad1N|i8errT20S? zJH)Dm6(q=bCtJfsFC&9;nlckE{WQxz*+yz$l^=IJjG5^=K~nTO(}n29=53{Mr%Xb< zAdzD~A*<-v8FKHay5O!w0i=94O-E)0$oZgbwNz`NfJ!f<0=CGUfC4W4YEi%#Od%NT z>`!n0CCPSswNMVD5k3#Ghs}AIA=Xa^yDiJ33L|#-|=*4m|V-odZhZEso$w z&O0Oj@dyibYt>QKvnf+7-Gf)R_~|1oIJ%SuYh(r6ev8_LG~&Y2E`$t$EGB)JaKb-4 z)`=>;t3I!LltndNlTEp$R9}UWe>$Gz!*%>HvUCg~rN1ZW*kJ}A%85&-nfyq19EzhV z77C+|X=#U%XetUyyyg3kvLG+t8|Zr8kC)2Y*MfElK90wrY%357kFtQ?>k&qZOd{9h zGkPWE118PrUMiS`)KC{BZi;k;p_60d=ClN9iu6NnNFzoz(7Io}^^v(>-LR6@ukO)a z>TKpDOIvDlQ}nC5`Dw5OE7*}6g!edlH#I78I7n{Z9oS%y&|72Sgxez}^;iVu^HTp~ zs0P@0wb|bpg_OEH9=uU5tL_wvw+m#4&#gY6kc+ux|B8HZE^@MdJIU3z6D?fby3X(9 zvXEMBdYa`;^|dZXG>{krn6`@Y`hi*2M(cRgF*N31umns~U3Eqj6{PaI+mrZ;W0((E zHHnr^ym^nhPHt{*5@hlL-&>Mjbd4w7$KuAdkBVE99U8)e2^NGd*UY5}bH3O6|7XN; z5kaspOYTE6J?ike$5EVGWQyW+aVZt2)&9E`QJjPsnOd5v8X8_v*Qf!G+)rf^#c31< z;4r_ZzeRCcsJ!LwCs?r8;lDxmM}sUfy_=DX6L)@U2OxAm!2){3i?CePpInoV9%+76 zoN{S#1RarXX7xU$B~(*n%3w;BYUwqAtU6{L@4-}1O}&R(FlY#F*G8(X)_Bqi`aEdm z)?E=p@aSW$CG7n;3v3u|hr<-PR{3O^g7O~p6GcfCPWaZl5Nvd9KIJ63E^LWhV*33g zv)AP>mY!25nLf}f=l`^rIz1^^B)oc&(+NW3p*H$zRwKR^tpLo(K zoTU!vz!#lD7q;;a{Ol>#q*TdNSWwx!PEcR)@~2Ty4?N(}r&$%3IhaH<)>%L6ox zXXp_20uh7GmC6|Ih>jF6=6Rzgd=xNr`2~FT8Rl=d z!K;8LtSjlG+c1`Ye~yI=-5X09!cF=#bV4y&j~cA*d?DLI@@OrF?$XyS#`JuNvXW4& zn#_sOqQ#hv{Gle(OO`K`lW5L|pJlZgo#`dZ>w0Py%Ew4H-$lK}t*C|0DIA#+m)sc5 zq^YQxyjJ)|YbHMx=eN$XYQcw_$Y_fZjhe|Mt(hd3ep)lx@-?q@4niyCG@+G5A#^>r zm?EL^Tst~ch@-kbmlBaj6S^1f1vU2*sF}RQZ=YlCUY8pS8K2^87s)tY>KZaGo6lX( zvl{NzdrBF1lZ?h_$!zDwyz_ait_95J)6O$bo#T8_P>ttkomAci=10~5)4_D^K`lV8 zqo&P-z160Jv(?P9WZWcztbV@%EW6@Lwd=C#nJ0u7nkR%8l}4YJWNyX|8deHnX$MQ+ zpCfzsfA)x9r@~;;KNU`RV;q$uWqz)P$>F9J;crsShVZ*d_^sGY3E|^l@{5F@Tt*5{ z2oN3}X60MJHRg%$;ZdhnNe|4ThNOg%keo6)EOkj3e@T(S2KgHg##($T{V7IUAdX4V_bV!-a^B zZOx`Nin7TRd9^V$SJZIT04T1sT>t+a1~nc3*$V&eh}by)bB0_zyqY7tV}-YqM*wCxWM|JyL`SCyo#o3e ze8pu}uld6hP$5k>JZ&JWP+Zd6aBK}_jb;yok3|}^fV0ahKz_aVY7vEN=MvSEGiDo z>0HL<4GABU=$!H$8e`f8U_(Q(8`LtJA|LfL&tU8?g{yec+921cnQYY`6QFX%vhQEA zGfP)`JH(Y1y}}84H6GbdJ&}36xc?PaqY48>(PPI{S2DDg+yE0LUobL{qiGORWUX#! z>06_;$8p;a_7$6qjF!2*Vu+f~HA)pab75~WOZlD*Dh)tFHOs3004SM+AP7!Fr*XoMZaWavv}(NZRTfgi z1Hilqm%%hftLKOWN*^p`O-vifq8(%s`6v3wS$xS=R=@4*TrIcXN66ehSI3;&Ib!k$ ztu^w1)-k@yZO^VUx6jcE#L_qBwnEM2f!ELyrfAgAyXtWb6K_nVhhCjiwo!(mx4saJ z(>8)wLpu6Xq8QWxC0cD6GQwbLiZT#pT?TgJNh|_V3213u0)8k1Lb$TCx48slWA$Gv z0aTki^raFID0cGJ+! zVNNa+K=(U~n>{Kaw?(Ke%5MNrMAsgCROyW3(X|B2f zEEZdZ|I=DkhGj$5s4JZC^s~f5z8Z(&X`b6LVZGkWK;bnhZ)X(ewgJW58s@#o6ap$_ z8cDz#m4$#~(LjpUiv(Pct+7(TD)2y8an{EYri><`}k>~0qAy1xg!XwW}c|KuSm^b5u$e;#u9tr-Bb<+4>fzdTG zECMZ`#{W@uDE2?dy4x4Q+IZ^DY2Nxa^C%y$Pz0=sAcM4nNjM>h)e&lR3!VA%yV!5J z>^AdqYgb>2G`$~*B-^C7*_KgMYgY{++6rN3c9C2rwi4zOmd9$Csb7sCv`Ozzp``99 z55a2SXg%_9zz?$3`EW;gn@ex$@9)u-6>szQTob_+eki`+amliki~UXBaJwJKG&M5V zHP_B`6&Gi^4v{%C=^v&_lAZq&-dySR@w!M}WkV{(o$3D`h4dgb4bbAly4?LP3m*x&< zg3OxNF+Bes^R|1SE8wN>V`#(~={|tB!6rYidv&GmOZOq0fjwsC{?4>RlmS)UmTCy6 zdXt&JrUrERq7Lyxpk(yME%cDT8XVPt{k(a*ZWeJGL?@HLsSVZsPc)H(Z97e@IL)<_ zoDzcMbSJ>A(r|g6C`sBDhV|z9z0MZ+Rz24Bj{MGDR#Ml^iNtB(wWf}gS1u{)bC}ztaLMteo|bZ7K9j1lp5OSW zlr=Wg5QbSH+&NfO?k#gs*<_JS`isH|uZ*z**{6rtmz?+8Ss}>r1Wk~Ya_*;IRFNP@ z)L}xkBuGC5Yl7U(<*go~^W3@+>Owu9V5!HCsmzjJwRN<%O&x1Xfl_-|Y}1DW!Zx*X z(_qc4V0#bz4_TIxH8Sac6Hd4-^#CACrYB^Hl~s6XlBjRG%>}|#>BGeVgOEB&w4U71 zpPIhH3_fiq^EMi3-pv%*b1NEPTEI|oJ_XX~XClCm)&-koTymluaC8^b+tfkvpc|Bu z-z(0kGcenYCVk+rk$s(0zJ3_QEn^50 ztT<4eqKUXnnoY+!$|7zCN^Xo>dGsshRw5nu=72eaQOdUictVp6#cyMN!r0f|F~4~VNr{q0nL3dAnp37A3*%i`Ts3^ZHype9(!Ae_a0;)^z<0SF{gSJ1i~&PP0%Ger>?*|;(7E3Y+s2(f2fiJ7JGxRfl)5@ z3jOpOIdWLyNbyO$IH%B|Vc{jVoKvS8L4a0pnxBHL#=XTGkr#d>1uWiR${!@%2d*0U zS>%lo2leJ)OIFdS?{2M~JEWZ^C!ifn8;TzKd~Db(inI!{KsVnxr>3A& z7M7JaUG~jfT0%Qdi$gmDXhcpUnnFM_tAL1BkHVidCo7KylpJ-==t9ib;6#%_$Ez!nnV|X zeV@p-peKQS^2#iLiqUpu zQ6Oi6iLk7sDP>VKe!_9@Jo@v0OJ%pON~NJ51^}te@_D7lQ1!U3RO|5av@nVpD1U0D z9IYUd;d(m?^pHQoyf7;MT0zXFf=GXm%eJJTcm=8sMe^GH)v!LI91g{YEQP;9Pw>bx zxIq*_<70K~aZv_mt+OhFU9>WoKxME4b}*4e1p><8YJUo}DucwCvuhuhyiu|YhD8OR3y1u2%XVIubEI$28E1OL507|XMV<*F4n4{>si!MxmqbC)nt#fQq8rZ zQn*QEN!|u39;Qfix@=U>Vo=Q}+$fHK-w+^r)F~dKN%n~-YRQhO2_L8sP)pVXy)BOVA@g6L> zQ8A;E{}gaT`cP5w%YWi+pW!yMD2=yK?TL9gt^Qz#rNE2qd56R?ig!X8Q1Zfe6?ux~ z1z9|Hd0rNeO_&@TB!%j|5XLXuA@yM28IqJoGLW+@|NTvhLfl2ZjoL7WHfU zRFeJLhzk6-JXXoERt4!VS%F{6!#$jP^}N9o3=Mn6^1)A7kmDFX32*7g7eB$(<8RCI zLcrY?VPY6_LEq1yB|+NIfR1@a0FQc#o2)izG?UBI^4O!i<12&~{bNyh3Ncrl*Bx_C zn%KI$8#{_Tws~ja5z{@U303m2B1&cK86Ia>b17cm!y@211^h_?t2QG*4$iBnN02Ar z;y{}y%?ut@`+W;IM(`ztQOH-@!7t`l!n*)Tje%F}4WJzj3kJdjT){pgVO*!-qI(IIl8yJvJt}w|#9k2--dJr}m*i#$(!8(4_mZl;J@NE=>Bp9Bwe81@}A1DYdZx z`J4z8mze1f2cy&i_7bq8fDMdw9(j)bD1*0&)_uf#@g6t8pN@*CA%lXAh!c;80D*^V zv9Q-g++TlL-x?_qfFAODhH%@HI;`3S>zDh;@Rx!*0M7>(;wqP^0l$GkB68V~rUWCh zxQvXDt`AI8c7rN#r%~jlcz090*GlA!wm!UZ0rM+04H)#Jz-c~wcmYNp1F&e9qrC9O zB~cVSE>tFcp7O=%VAKe7E7vWG^Lw#^_7}350JQrw`@>V8Bx(z`xLa119Pxzam146$Ibjkq3 zv|JI3sOKmQ=%oqmC>5(%JjF}N-hvk2BR%@%C>!uq8X;@75Q8yS>0Sg~3i@$Ff6ke&!)(?GIH4tPlrYU!&w=%pGYcW+$XwTk+1pG`EH zxKK{1iSHccsYXdHqFn+EQZ$hdh?b*N)p2qW|1Q|QjmzGGg-+jT%RSIGyHRDD6e zi==pt@+nPujk1a?*hfy9GFifiGa|B+%DpJ&({fiz{F-I7L=mYtz6ou)hDMbKvp+=W zIt8H)8L9SWO5!g?orlp%Q=TIx06FBODZI4sb?JvzBTZQ)eABat5H5a%gUbU+*Tjj6 zST9kQAyO-ZQRhEL89`8l426rqF#*@(lk#pzjxr6O$kUG~O@#CgF4?pkWuX>rK3qQ; ztqE8}f&hBq3$gxj1vbfe3elkROYp-RV1i4QhE1+WT{2}2u66i;=WAvl94z*QL-%2R z1}H=Z`SwLGI5@I0m`4K=Ilh937bJkwvydj5o-fp(y~4efufP&)JDEqsNrsOg??Kry zT)bcV&g{=ct;PLCyo$U%q#?w{gXz%OFYG_Ju2(nZ{a3 zL%3SS%PS&sn7*J__ANf(+y@nS^-JJ7yM}Lu0|vvZH54_ZC%jO_!QP_043fUhf1rsl zl9nLR18n0&gxA1&fctc;#lLZtt}qR$2sVI74F~D#Q+eue{EKLA*2r5B8P|j&*cHgF zfefm6&)cHSz5kD|>i~=D=-RNbU}G+prASpl0YyOtv7msWporLeizR9-*t=k?8(hU$ z#zYf)jWL?oD>g93*n8JlE_SdRmH$06ci9E={XCD}y)&oHIdi7nnKQ`C)E|b}2qqbp zLKW2vg5qyMIu##T!zf1^uQY`WW&rdeM!{c3nHK+ zL4q3^G180!kb%)d3X@B8GleYXOTG0Es|)fgfaD7ttps40fs_D|a)&q|q<6jIQMIQ`-zG+Ra!_lYNo>ra5zVpI>2w zXo*k~=}8iSaLIOugzYL2LW(tuG7`fX866}BA)BpZ)_-m&nb?J}A%y6u3YqvkAW+74 zD0L4>C-6V$Y$G};`p={z5UK?b5>D|Jqp>GQ?gwOR`HX_|Ck-K7LuicBfZJmvCl^BK zA`nRX#3AfIgh;6`WhI-9k+PECnaiB1Cl}7I1O^FAhE4@=rd|hFGA9{&rbzLn6fpa* z65pVe{w#r%E_u~R0XUVqGD@APrz%ju=vyR)Ez%cBjOrS~Fo8f-pa8;DiC}~DWfCD2 zKh4E4oA4_@Vf2HX6q41t#Ac92l8}mmt&@b}|AUafAoPrM1Ul^jL4|4JB`k6s$(w;| zZNm4}i1Skop`AyH#ONzAt|5JAA&e6OgQ^3uX#0f_7`-4b16c6ml>+!V0YlM*bPO37 zU;>R&is5-$q*RtEDf$5XxQwL#VY7CEVpW0$1Xa?)%B%|rkS4FWE$Kw#C+XpiI~kQ4 z&H^@4B!Yr;mD(TBjnXhq3*Q%gfwk7|RuwE5pk7FX|1wzyptL#!lGNK57IlTB^9Fi( zC+Vb^Z8oPY$yx+R1!Nr~FcKUN11T-uz9l&;*r$HWOg{M!8Twxeg7tGxg-=WUKmknyHv; z^&W{%NVM{&1g3<=;WvqwF2p0ZwGz*ypDQt_xeJlfHPH~72n4D=1rYj31Ow6|Btl~i z;h77mxKaTGOycwr5`#(*B{5?0v#{tcfXWp81{JRLe?YveB6LH|U7V!TRYJYAu1i+{ zr8QijlR`s4D*j>!2Q^Z2N0Y#O46RN;%r!s+#(*z4XXzgSs{^nAHC>Rm2M}3>8m=D5 z(7XUCK1k^&QmD?`rXVR2Ka!E8*^H!_?MkX@K@j3A&rQ}NCd(ytSKf$h#_Q< zqybA|h!>TGrqE@k*rZBA=343{tV(@L0nV8cgZj*w5`iiyiR$M@qKZmqE$WXFft){G zB9O@lp`j>SO}v3aie+FlNMexlzmpiP@UyV!%>boUA{4I27Z9=`qM;`ARUW{a$%u1| z5Y8Ouev~W->RC(v7GQ@y{q0a;TK4$g$SN zIuOm&_a;b4Lnb4HNUCv|#8W9}7Qz@WFnVbiWbeXy=n4q1BTq>OGU$?<6^5!S=uj0P z&SWw`h~b^qBBbbjC81#CEs}(&NF<<(fhZw4W#_)?}mM6lDE$DtB}>d;O= zWa@<%Kw2tAhDf0*L#|0eAJd`#0W<2}FnOT90Qxyigv~8xMIjXlOl7(SsGb@W=6KWT zCs<=1tG5LpAkJy;-%=|AHq_kF%o&I?HAHJD9_PrIZlMRCU4(g*kYf@Q-|$?|dlY9L z&YMHI@)gvu|0tTT!WjyUb8pGpT<@J$M*hY3Oj^g&vf!P+cx z(0=F#Ftmt9eJ_zJ)*y}wNPyJ(=m2ltj2yZU!4_|zdLJ1#n_*6mYad{5BGHU)P?5#uOeA;m_ zZ_5A!&vat`x}0zMQzsUp%PYx!7)!K!z9N_3{@0;BTd)G3K(xzZjVt&C#=c_yR%z}l zc&vi7Z4@9SZTx9`ol(~>=i3z4T^F~UyEwC!%Ijqo8rjQodAkP=J(X!P@dY7_MdHHF zxX#N5KSKV{w`WI}3KS8Wf7epJx+GY%m1(yLBYr72lw!Sf_kQN%O0iD5nLqQ3rC81I zyS9juk{0$49?JIixKrs3ou=5>_Z9YHx5X>I)!<;~QT0cph|kQK>a7wlc~oiE)S(qN zoA*uUNjtIlOtE@!gtkCH{Tr>gZ5ifox4o4E1=Mel(&!-8VrjE-+Gm^&zzMBhA75j` zTn22{vK4(~wL~^DN7OHe4WjUiQA@=4LK#v2la2YXu!jXJeHmGtqQ6lb_8ic>1oozjL|!UcHCz^2@@J^G!M_*1jP6`1)DwJPVzbOOf*fQWCYyNmiM; zIC%cWSK|w0ckCz>kFeScL@iL?K1ApOEp&coCrws$E_UTZ_yOh2r}$ElWqpe8!+dQ= zvAsI|Os>2obqy*$#c#V9X!*e16q3ZFGRs?12M*9cezXE%LRi`bgT25f_I|P3G7Cwl zwk^R?Ek)9b(E0tIL&8F#^Iv5nR|}oLthw0eWR9HwZF8zgUmO<@k@KGg@?>}BR?5CS zg%9Ya-->5nrM-R?4g$Kf)+K!^;`q)Kx=00wSubWUtH(dPGnYCu>%Ky(|2!i%P6^5& zytQi~DBuqFmDh43xFjMpI;*Gr3*aZ!nTB9a^>jB5iA*?n{iPV#-hVQ76Tx zdzYS{?S)z9RC@=NX6b5G;;!Dnoj!{9^9HUvc6tMM*r`RVe!5JR2SIctl~$A4E!Fp2~rNJrF@ipr(gZ z{sQaDaH8`C*8YLM&T7}gd}sx>!n5NLtv19c+Xn_XtLqQJ0%9*m$`D@7kG<0UaGm@6 zvnFoGUdhVv&Jz%5StCSaGGG#)>d$KG?633P{wyk_js`VAgNhSS*C$fo1kdN$w%9F0 zmlmYe^rIeb{xu#{5m?7xO4=VhByH3bY>^#k##)a3kNH^7RljTe$BHbbY6lH#riK+I zus%#6UR|ErWKY6bCdui!y7ekARtY$2uH@ua0Oz}WoRj%DxWDfzA6E&|jni;8l$FxS zGIX-eN$GwW&l?6{=$n_>`O=QgT`CEf0b+6#O1LN5g9INT)}q1pk3Db%I(4&un7$NT*~;Vv%lxQA@2T}$w6kk(Gdn7I-Z6?O`J>9zPm+a82r(pSoP-b3#&6vUHU%mU7Zbf>+mh) zGa34cN7&wR!cOYAzxk@_EUHHm>_?-gR7EOSGzc$~F`^lpzkyhYM`Q8|8gOfC;7kAu z3A25HLER^~{ZIowfY3@cc%vGuLPQ@C$fynOnHo=*pO_-cpn=m0*$73CrHIug!?CfX zN`S>1X*OCdmoEi&rLtRy1BT+3v%OSS?8c*VtAFjmQNA>s-Ez*hE5kq4U|)Am#iAXg zNXJ9nh>=S6Hp?bdapN#<<`7|z5cyK=d`5`$3N2W^NY1MN??R+?W28ukn@f8zl^lMc zCiBsK{*B+R$=q~Xck>T5*@#}FsOFdq)A3MO90x-ew2=u^%SmtR?nxzIE*uS^&EmmzSkpTD zZb>UXb%a8-+thpZr{CWokn%mn*bVg`8Z&8;hHg{42em~$;9G!Sv}F$w`}L|Px39}; z6+3f=6#HBqo5^GAvSu!;Zc6g!9f+iyZpHQEYwEJ9Mb%5MDEzRk=Z+h7nU`(E_n&RW zoFOL97IXg*bV_`$Uk|jzG}oVU#8y3c1(W^StNd=Gu~MdugJ3}K@|;(XWNjmkmO+b0 zjp1Q+!(X=IMvJMUjdtSiu{&aWMqDrZ$#J&F>gU}?quBlw?Yz%Z^A7U^k*u>v+3Qly zruI~8w))X(rf4;G_2FUlS!%+LTgX70xpW+L@)@YntJ|Bm{=#Q>lFz`>8i?*?_s1`RXb6l<#UXv2SwV;!6Ot^r+B z*!$LG5$NTpI`}@N7-ra0Gd2oIJ#Zt){Nr6H@WOK#0G zny@la%7GF6L<|k}1+8Fda>ogcPeO4Mj942pI%GC3qEk;^5QS-iGc&L^DRJXIQert? zqY?A9y}}PPVTLGW|A+udvEvsMA7U}bnqnjF{#azSQ-3@sSd=ys%2dbfAr?NoPh&Jj{w=w&F$)Oy_<~SiAwuuZ3PQs$ zC)U(>X}2Wg&;*3+ldQy=M(cY8#44R+EtXF0C8=G<#9N~ltx;P-)CRX8YF^>be=baD zVvB-={yrnbYHT6&%PuKa^*Ff5$B(>K99$wPLAu0+pNL>s)r5SPsBnW)4D~)@Syyw3 z9+!km=&fC1$!WpIFjfdmE-`K=xr7hz70<$T4F~bX@ocPJhNn1=+U9{g6Y=A7>SnQi zwPg=+iEMgJ>x}78@x&!a6W#A{Bx{HHa=!wG)d>idMz_?obpMg3G-V#TQmgsRXNUnY zYFlwve!MBGmvXbe=%k;CM_7y?@LZ)4R`wP;z* zvltVRtcmflXF)OEnZ@Y;15rRgHYCWy!cKI?2Zvyhpn9&7!mV${T6B8SPssIAJi?L$ z#pf!+Ifv}7kc-9y?k8mn82=^*ox=ms`0c|fpzMX13=qJ(za>C`0FY~lOWN~R2`oZ) z_8Om+z&bd_^>MIK&u!(86PTZq7hd9wpA*nfpFM}W54`5Jpj3g}6T^uYFWz&`(VSDHe9q1k!9-8!xD)Rw}sL5^I_;-mcKo{i3 zw53P8NYQV5 z#QV2kRjaqfy)l#?nMz}O*dTJeI2ntP2XG<5wVRR@#e%~t>fWEgE2ale!n={dZIdwW zSbLXON@7=w72hjdMEzqkFW!>HxI8>2Rpgpuv2j`}-m4`G)WsR(foy*|w`Ot*GVqNp znOCiEG?c|JEGSAVR0{=bB~&e%Y5?N|fK>almJ3_4Z*^C3@nI|0uFO}rg~K*OL9?%*BZ+^fNTSJJ~=lEDWqrdA0(6nSXYPnLypyPuY{x_6^4$4<{ zP)!gCnX>83DMedUT_~T1Y7f+Fzw&=tV^(_a1li5-X(xeB@G3%s34&MuI>9TnVdWeH z&Y`}pJi(i_VNHtG=_W=`D?Rz*HmtIr%Uv2aB9O3{jb*Ca_mg76eiNr)QHmq#`HlQh z8|D#|H4IelViFLKnffX)97QmY?X=c4XCe>g`mH3d)|Mr8ADkeqXhRhud7rdh+@9AM zYvSt9IMXaYU$QU0bilha$eEiVlMGyeo@o>OXcoP*v!p^>LDZ`U%(d>FGr!rEl?^^| zSlW8vlN|6y6nmWC!r>zt`OrluaVG6)bXLbc=Kk$iVt7wyhz$d!ITj4L{|>vL)r8%| z+B}NJlu8bz!<_m0cIYPlYR3O=hoQ8xS4#SD7k}FhL+M4|aF_OMtj_5PpWB`d(%l@r zumh`D>*7ucwuzpY0{hBR;nPeJwJan3ful`l)}3VOIPCzUS;QC-Up{Lc=oO451?(Ex|LOse~^OL98913qQ)KkP&M^>#tQijG=g+lYc{M6A>uZmPL$wIBw*99BvFFzHW z_#Yiv`4XcCAsq&ER&T83cRI4>DGnNea1b!JyBp$BU+Ck4RE@FG4SZhgg2fKwAzJn+ zexH;!&@U<>t+!tUeGCdoz-vHuF6N+p)75`}1v)+TJtTw`axpj!uj@bFrd;Kwh`NNB!X*ztD-*?z&$zIa>6DGaINPM-Dii zKarj4N81S0MQJ3lIfMdTIXfLNavi~;m0U+K$J25~^`k#^xRHO;8Kbmv^SP-ri}C2Q zUut;41JbY-X_N-x_Hq1CXI8s$Nev)c1E9~gqNCc}#Qa?-KC20=PRUO5r~;V`E5#%1 zZIWD;*tU}Q@50I#tG!P$yKu_U{jk@$M|E()4>)7iU+6nK& z{B&0~TG#d-kMG7B8Zv_9+!kL4a65QhS_fbCr&4@*Hx?0XcbTd#tdXg%!+|^a#B!V; zrI;N^$;8-Le0M!Um60OgnI;^;+y+ZZCd00N;B@{VFWa3B@ZI_Htypdp@}TuYRCMEE zB@l6)s3{DZ!&h}@Lz7$)fG8DltdR!iIf{$6gcc*a-!*s13#JR>&qhEO8GB+XvaG+I z?d_s{J5#?i%1>u58+J)8U%g}LloK0riYy;9Sj+p_Dz8+-{7xD7oPl1U2qhh2$fjYX zk)ygSBOZ#VctimAwv*86owjV?`3>hPb@!s&*Z0jVzyUp zr+DN$Mqc&po9j16G0MSd^8{a6>Fh;bci4M3LRG7KK}eG z)=am;j@RqOYWr;1B%2;LYTC#BCYzpCzlx@(p`8O?{Vqr9&#jxV|F@B^M>*Brf z7Wzlbv8c6Mw(S+&H0sphl9+!x6}imo_huDzi*NH@y|J{=H<&N#&F1R#1O36kiowL8ia1Aw}wXGQaHavop#nesvZIvZDa-E8dDSbfp=Qn5VrP}8--Ws%O;1b*US~f zJ5^K=S3t=zo743IW46ZaC<8Gq4O4P?&o0INT*Cj5QuV!NY5cC%lR+*2{axl&w#r*t zFTZ+Z+pnTGD0kH&KJIHQQie0W=xbKX_60xvHS_iC@(8Opa%CcV zxm=kjMm?j+snaQ5q8}@p(inp#a+PHLfk(Q z2N+w7JJsocgCo&97Dlv)>?N<7mDFUCP*N0y9T5ZDaAa8xY)cG5Gz_7o`jds0{0&>A zBIaDND9Ns_RFa>T*TyRESe^e+l8T5FFlVVc^a%T#-cW+S7xI?<*{V|QZ&D3JoMiHO z)IxrPI}TtKymP;LhbAptG&x7rGJwK$!W0JvFmLYixB;v~ttWKRC_17Zai%gifDig( z&<-7qH;q6y_Y8u8?+mQHkW$^C6;hJxc7AC98)DbE*h?P&4QuSQ?>f<@xXP;TI$!b) z^QhRTC#|c$rMPcc`&&ko(?Zk~0r*IR?Rkga_=fc@7Jo`ESG_;Qn-64BtjP!H8v`RS zjZE9f7Y}5?u48E#9j(G`8WvPqBYiV(CBHS0O^kA;AwHPDYeq{-nLzuD)kgUxy3JU! zvk=6juM|kDNNrKVSWuhqHxt!CZ8d=}8N`BvzS$^EwfH=l>WyeqlyH~>%R2VZdxfQ< z{Z{{)$6pR&(H=G$d|;o(lFj)Fg|N_iR0f6W|ed`;`lFvF~lhz$IlIBvs^B0 zkc{-_EbZ#pntbFCR>829mW*J`9w@)4o>}VL8~oQHtgO>CWNDh)aEG5C!kW93)+jYR zYpGp3YVcagtU_2W76mPoVE8Qc6x5&n8RHCDE!0yQh`HuGzs=_)V?4O{JpUsZ4ds9e z+=x^Erm?>=bVyUU{bUt$^i}&b- zww%5q%sVYLwb`sGU3LCsC<}2+!j>9!@f==Z7%T6X^&?(ZY~l&SSYzGpvwZ$AmZ&RM zUW)Sba3RXua`~b>!8$KQ(Vw-55{NMrjHZXon|_d@w42TQ4TmUG)<{wQI4MP0W)`J< z2tP5LwGKC-kfP=!rj5YY{hx;kewP*rGScZc03^YP*mI~wU}LBDH}OrQS!JF&g1LD7 zyjpTN`M2bdYUU7Gjc*yj8b@y{uW`U<#A`2OluljNIroiMCf!u`3>C~?aVut7fG)sn z`Yg$;*GMqivr001d_poCV`kQ=DxW`+H4JYHMOhW+0ey`zh51->7zPk44mx#9n&c2M z3LGA+lpI_%4#{Q?(}Jlu20m#N3k;v;Yi84H#5!lyMY3tJ(5fIS08)U>o$n-@BBQ~^ zMPt+WFIkX&W;WZZNH&S1SzxVgK4vyzpdMkArPwkCr5LnaviRvw$)baq zMTyG%F0qI$tFdUTY$p~A1_>4?=UK7n37AGR^Q)Jw+G8f~HWm%ef@PBW(PNT&l9_tV zAijGni&PAf4CWK4xjScYhjFa2hq_c^xoKEC%~&%6`Pbv{vC8EwpE-`zDr35Y-qjT5 z&``)idjcO%=jX<;M32%MiH2E{v74F1?*Y8tc#s$x!@G`W?m?sIu0VXcc>w^~%{GF+ zif-pQf>F@;6y59tHE}gxHlEE4?~TK$VgXJ4aR4bIa^4ee_lOAprGIkfs27e2YnkUL;e)lnfj?49c)U` zVd;~UfHSLxuQ3cz0w?&XbmaMNK)fjqo7jh3!DiTQfTcge&xST%{9!8&8Xw8jdvD?? zlbBc0WV&1hF@-zw)st92N*>|~VVCR}EV2;!ED*x|6^4HFt_(baS1(V0(!AlPUj^bO ziO^mE48dnu!>oB2Yfe7siw=tpHD|;bBYOfUesYHBR!l>CfPo{}f|$PACP>sA$8h7P zWb{A~BSVW{c#9O~lF|eyRGwRLrn#Fz3TlbOm=JrOgTa^xpMcoq5LF6Co|_^GM9<+3 zrMAHv{o!0Oe$jS+Z#-!ti31H{JVJwSYk3{WJ&A41}> z$R?{qI`6S1xc5|C(9glzFZOOn)<$@%e7(@I{abXvc{pJ~DLAxjXb%k{&1(oSaXKCF z6wF`&p7fWJslN!;RQPjv(jSheYbiN+N_CV1L4r~6lzqP_-UW7$6J_VFH5v2Sp>;l$%<86S_lny(Zw= zorYJZQZ+F~E(a*0fH-3rC12A-BATqJvAZw_b=ep;ovZ`NF2|X6BxAluU{nPN?53GR zw%}mUk%Y}tU?lKxp<+zVqB9_n!3Ge!ONrl3B?NRL$uk%00I0pikb=iyyg^SLKpu@I ztQLx2sDdAm?uOI#5QQxE0#B*?GsvfulX#Nu@pI2FDZmlf^8p9!t{Pe=Jk7Nn<N^^6 z(05tzf<@1*gnyAfnn7ftwqcyyBC-aMQ9)mTEw%SIQ1l2Y6?U`D%F82+j(`vt>GKK0 z@B>IeS-CJI=?{+NR9_^uI1TWsUY*SFB?X7DIwCXd@CsqUfj6O!OOTC7&)LZHrlK9Y zM0xoQbe7%&C{hoz07<1b;we)$Xen#)l$trFr5weRGz0J${FD{j1HYh#so(%LMB+g$ zSRFhixyQ&)Ar2~j$fTku0HrwKNgU9=yj?B@eh!lfU5uJ1bU{L(@5lfY%41jt4640n zg(^uq*fHjG70BkkiV8_3-wwp+oo;X+fTj%b`g*OX)`lTqKorgZfvCb9ZGbLGnSM{D zPRbbBktoX8LU3jeyn#7YBGmDx;^8%YnQ*TLc%Ue+$oW7nQ~$Sg!#7BWK7PZa6de)V zh#&Nqsi#^X&!@apHgyh(=Iw) z2l5Rz@&hxNN6I14!)s-YEN;ni$^&G+T(H9sK`^Wz*Z{JpCfBLB$pwC%9+nTY$dQaWa099mpQeG87QSVWcE7i01H@^V`=g(y$+X5{4a)Hv&z@ zll~BMMnj&2r{u6yOId;^SrI028i?O`22+ckssEKAFk+bCmx9AoPN&gblLGr;85k-q z3fj=_kL0`G?)bZlraAI1(->o2Dhk%2Q?El@s!$2(UU5Wq{CAtRnSs=@g#*JQ#coX zX6o0FHQFbOb5P7#TbB_xJfAk<-lyAMKbg8)P7~7+!Eyw!SZ(^p~k`0(PX&MtI7UK3d9Gcxw7o@KZKhDJmI}B3<3a+6sI!?qCf@lwZZ<;6AdEHe06A#D&fs8lK@D-&0PbX{(`ST?QC&B!l4#oYQbh$GONj zQygNO(?MAMF|ed;mHwlxr8|TSVW%Z1rlVo6Cu3xorN6u0A;qkcIiv>^FH_%7R_m4k zN0lu`4;Zxqo|*c+SZX2l?81}&FtXEX|64p|iknbKmfnD;Y!Je=lp1(agOI6jqouS= z#1C>BRFtnkftrd(@)=wTStw3%NRJ9%6K^;OISrX{6b&#-KNvrylQqFF1cBCvaPDnl zQMG@b0TZNoEyaUQ3C0=e5XD0*7OQQZ;HacHDM+vM{XYgN*la{!wq|_Kxrx=+IsU*e znef3l;CueYgl%lL-*d0o%)8dX=@<~Eg&o2pTG`$Phu_3`LwvM06Qz|_T59+jDPr$7 zww2B0!)D_XUFahI>ueBRv523TjaA92bNP$e_;yibE_a#3%DIFsqCk<2Ft<>=Kg6Xq z!FFlGoejMC9QLb2E>b+PR&(yl|cL4paCVwZx~`_L8ru+G`v6x(u(Ptg-TaGi6Vq zlXO%OLXfB#**Uv{H?KR7m969`AjhMnC@oYw~v#{XQjs$IcHj%~*>qSOw1t z51TAk3QKLk@+H1wou*nV&kFmj&k@#6Fa<9JRqQcwEGEwim(`GV1H!Un#9m8XB~r0F zKh8+I3iM!Yy<_C*-+Fmg_+V8uzS!e|wu#OPpPi9QXN3n88#z{%x%p^$SlB+Wz+qv7 zK+8WY{N`)2Gz|S;hlPtKAv6E5a8Rl&Yo~P(TiZx?vM52w!@?U*=;^Sqr(xbG+A$+e z3wIy`m<*GVB~A-3Ur(07HpdR%kiH{xzRTa{h)WU^h#0NVmQAB>I~JXb{)bX@TG++c zp(!C?pB6N%U4a7Nsp4JT)#i&c#7-$v+Q_xEhATIOB6tzPgjXQ44XNa#EwGyZ$n~5TAz(A&oT1C-L#%8~VIHx*pVO_=(T`iA~Z)uH_|v zW_~qYCkYLPyus4_7{II3<0v7lI})g3V1#fUeu_nZ`dEpNgR0?M-v4J7>iXk&$^H0B z;x5%#U!Tjj{EXujt=905M83*ILH;Oqj7sv0n+Wo+XbY(%pDjM!=aUbb%-bvl`QOG# z^0!u4$hWr7OHnPgxsiT6g6g?eopQ19*P)$vZrF0CS$ZuLBIU#KjnvPgf z9`CoD`TMOMBUxQqX0c2&P>Y)q`=Us^@dUP(I#)op|nVD=IVDcxuOWHk#9E(TJ3y_VH*>7wP$vdXjh$e*pnXJi{KFT^Tu)xRXwN*Y;F+k~!Z zj~G^&3>$oCGBAZ=rD2;J_I8XN!$+=T9bCR1Dn;WzSlAc;m;bX4?7g+TM60|*A9$7Z ztg*|XWQmu%$b#4R1E0B`^>=Bg4?Q7m-6IiePM&@0bXTfXsny=gh zJZ~*8(JJrID_-R{vd{pDm;1d1ukR~9^EWm|m#~2AHnVCjb{b3#E10z}d6Uh=x4*`B zt_8o@OTKb5@I&WwuPv;am)cLlxLLu>$_0GZlny$cxQTi4XYK(vzvtkWFA>#{lOHuthbco^el@e8a>O&d%Ui1I_0No z`J1is{g9uHYZdbNa$9?!T0dc9dA^t}P&?G)AwtZ<-lq|HIj32A+#HZO*9x1B7&^;J*F-tRkNFZXFJdYr@O*gE*| zQ9GF%n@&VbhBQ3XZ=3KXJ6VWJKMiY<(PHBgkNMr5EYxXN7&X|q(&o9kDUAE>VjeYH z8uR6g{M!z6QiFwjwdc_AFca1u0*L0uNzU`q43~l~|jL08mS|}Tc^06v{^5oe>nd;FFKr}Z_1m&)vjCzz=jR)*z9ZD|O3DSS5 zIM#`v4W?K=cQ-4YlHOgj#|(=?YG%V8n`E^~*7BDj-+T{T8RTbQ!*)~c?j5RLaT?qh zf|HXLIKm_BZO@)9AhFg$aK0K`lH^xX_{$Lj^Q7JCK{I*HJ#0u={KWi%2LJgI+Hk8Z zO+3qN;)jz8nb@VOG;z&tvP8Y7TS|2Kp=RRny)bbm5iuG5!b3gOP?~sMSBZ6bn#IHk z5BZ9{F!6hGR%zmAq5SqCUof zHd8GM8FAlY;>}wA?I{)$w?Tek6VKG(b`o5^iGv8TfQfr(aOr@9k3|X-+Y^{D@c@X? zx2m~P({f34`{;JjZKK*>8VADFz_;NojH}X5BN|dovE5-0y2Uv7W8MMS^ zy`Oyw?KG^En|GiV0HtHqM842EoJtMCi7SNTMGK34hmHI~R0mWdo_-i-cP|`Z?J7;a z1Xa@*I~O~sasGtj31RSxpP>kJhRycmP)jcH51GuvsXN*j^e{+Q+f?P_GFZoYyR6!w zu#Zr>DRRvW)P+0#1d83Hv>bdMK+TQ)otvi(g+pQoQvx{M3gF5?)<16jzrteY?vTZD z)CKiXi7@sGfE01g6qaLJve?xf`8ZXp6%ZAt>{SnI7Hl%lIEaV=WdiusgKV%dD*O7aC4%`IW^|$|F)vaOQg$Y;1)B1{(k{v%$P!%Yp{0hZNKZ zeF`@oW)({N0vE4WrjjchfpV1;e&8^3(YaRSlaI3yx8QcNm|Z7Xi+k!8_c_8UyFF?v zbITUUoppA1!OzmWZ9P-E0m zs4KK7>APvX+h2$Yf-6Gr9AQCjff|pF6Rjoq>n8vEFXr#^q>Yro6S?N$O8QOS?I?3E z`_mNBDjmidG4%)R$#85Z^VKZycV9}Fd5~F2AzD*s0pAiJflH*=@~_Vg|@85HR&0zYklDv zW29#^8&}XX#$&lPpT*#D(lbWj_AGdY>wv5Io)HL&C=$Fedn@mLh`G6VwUowbKGxzR z2d_#Wd7LEkJVc)KkvUhTkL=O%9*?m$dDpAbN50c?cO%#Ac~!4UAK8MI(rmLK{|Fzc zNSz&)cdbo#9AIYCO@dVmnr;bFP&fLF;ep3laOqLB%8N@TCz*W&8Nx>f`A8p`(n1#H zkI~lR`d^Vg5~bxP7s$PPS^CJoIl?Obmm%b%nqT-xrEvv(q#vwJK5~1K^pRfP(nnGf zr3Al?vX-FSW#J=DVzk_0$Tj=OgG)Ji@mY=O`xf?F%)$RqC7szbIlY`9+{PXEC#lbC z&{7w<4ZL8X-y>&XRl&KoR!q!>48a7k8hM*x@ILkycne%Yt;#wXdg zvAvIJ3ZTOKs;^xU*u+xs>Bx;Ub`{G>SS8ZMArg}>DOo)M+i9ve=);{(u||%2BC)r= zZ*|_`6sGs>^>S;8obkt@gWBgGh=x^P-AZ-_56T959mE+lIhV-Y0f%?R)Kw8k&uO{4gL(=4{) z%CkfeYt%Rw=F~?}KOVjFxTrcMu(U;WUf%-EAQ2;BY2!rnL{rccPp->~-VS z&#qIi_##I ztsw86fg$G=tQc+@TpZx?1N-bDz$O5c*RMhw#$;&86d5BP!H3b&qPTyG{?_RwK77R) zU8UN}jUu&SZ901f@YTD?e8O4mzMear|8y2(hqYDtjJQ^*xxpmIN zsXQxH^etIH73~3ORz-2trGlymq}qk3vjer-&R1Swp0%7nBEjU8h^`u$2ZouO>(>Bl zj@pSCTo*r#7!fi|R|W*8T?4f5L;mIh3-EDiBumhEpmhm$;?xL$t~}t87g zEdgoj94SE-KH#YrF}$1cfVdmBoIn#b3iFKYtshZ0HV)*!Uu02Le;f{FFCZnEA~$7d z?T9v3-j6t4azr3UVka}W?y(GE&k(p`)3CIs~{9Ygz{H ze~HzwPao`H!fh#}eD-Ao&Z}OM@4d{Lx>SwUs$PGK|DHY0J+H8` z>?TE=Fd6Le#YMeZkvF=+(o=dwN%-`B1TV+a_Bb1oon!H0lzDD5ODS5MLn$&4KFg{U zTaa%lMK9;VrI?HqkZIZv&Gi1(BYIcu-Dgx)crkB%m6fmA9>iepMR!FNDvFpDauU=4 z;LVMRm~u=Ks|zil@8;78bkJSC{wk~F)4PEbfBx6j^~d2)QGY7k<m_C|V|lklDp3`q#&=`B&H2SB8H59c;2wVHFCJDPn1< z_x<_Mhs>3~xW=lxmZ~qMi0MmGXtB$-W%1hAS!2gwAgdAneEM}ZLKm}zf4t73bjdq- z{Tnz(k?6;hZ?HtyQS~H+HNCAUxM%VUH&|EQcNKWen=IaKM_mc@u$OgVJ09f=Zn9uq zPz*nElO-5D`(lyX5{NJXVF2BKt-Q!>JZmoIb#61?lI53E6l~cKss2k|=JC0eZA(Y?C5I^bRetvu zHtjkz7~vqSo%6N1EvN*D=0>czTiSw}MRWO{+pJ?dCtyJJwGILCBgVYh_816QEy&o^ zmZc8qX|dj_LzY?p?FgCgY?VLt5Fc=dHL~+Y__i%~5a=eoF+X_+;ag%)^Xxm ANj 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 M!!jQ&x+){|f3DAXq5uE@ diff --git a/master/nimlite/numpy.nim b/master/nimlite/numpy.nim index 015d7c34..883e6d0e 100644 --- a/master/nimlite/numpy.nim +++ b/master/nimlite/numpy.nim @@ -22,8 +22,10 @@ type NDArrayTypeDescriptor = enum D_BOOLEAN D_INT D_FLOAT - D_TIME D_DATE_DAYS + D_TIME_SECONDS + D_TIME_MILISECONDS + D_TIME_MICROSECONDS D_DATETIME_SECONDS D_DATETIME_MILISECONDS D_DATETIME_MICROSECONDS @@ -523,16 +525,20 @@ proc consumeDescr(header: var string, header_len: int, offset: var int): NDArray descriptor = NDArrayTypeDescriptor.D_OBJECT of 'm': case dt_descriptor: + of "us": NDArrayTypeDescriptor.D_TIME_MICROSECONDS + of "ms": NDArrayTypeDescriptor.D_TIME_MILISECONDS + of "s": NDArrayTypeDescriptor.D_TIME_SECONDS else: implement(descr) of 'M': - case dt_descriptor: - of "D": - size = 8 - descriptor = NDArrayTypeDescriptor.D_DATE_DAYS - of "us": - size = 8 - descriptor = NDArrayTypeDescriptor.D_DATETIME_MICROSECONDS - else: implement(descr) + size = 8 + descriptor = ( + case dt_descriptor: + of "D": NDArrayTypeDescriptor.D_DATE_DAYS + of "us": NDArrayTypeDescriptor.D_DATETIME_MICROSECONDS + of "ms": NDArrayTypeDescriptor.D_DATETIME_MILISECONDS + of "s": NDArrayTypeDescriptor.D_DATETIME_SECONDS + else: implement(descr) + ) else: size = parseInt(descr[type_offset+1..descr.len-1]) @@ -659,6 +665,33 @@ proc newDateTimeArray_Microseconds(fh: var File, endianness: Endianness, shape: return DateTimeNDArray(buf: buf, shape: shape) +proc newTimeArray_Seconds(fh: var File, endianness: Endianness, shape: var Shape): ObjectNDArray {.inline.} = + let data = readPrimitiveBuffer[int64](fh, shape) + let dtypes = {K_TIME: data.len}.toTable + let buf = collect: + for v in data: + newPY_Object(seconds2Duration(float v)) + + return ObjectNDArray(buf: buf, shape: shape, dtypes: dtypes) + +proc newTimeArray_Miliseconds(fh: var File, endianness: Endianness, shape: var Shape): ObjectNDArray {.inline.} = + let data = readPrimitiveBuffer[int64](fh, shape) + let dtypes = {K_TIME: data.len}.toTable + let buf = collect: + for v in data: + newPY_Object(seconds2Duration(float v * 1_000)) + + return ObjectNDArray(buf: buf, shape: shape, dtypes: dtypes) + +proc newTimeArray_Microseconds(fh: var File, endianness: Endianness, shape: var Shape): ObjectNDArray {.inline.} = + let data = readPrimitiveBuffer[int64](fh, shape) + let dtypes = {K_TIME: data.len}.toTable + let buf = collect: + for v in data: + newPY_Object(seconds2Duration(float v * 1_000_000)) + + return ObjectNDArray(buf: buf, shape: shape, dtypes: dtypes) + template newFloatNDArray(fh: var File, endianness: Endianness, size: int, shape: var Shape) = case size: of 4: Float32NDArray(buf: readPrimitiveBuffer[float32](fh, shape), shape: shape) @@ -711,20 +744,22 @@ proc readPageInfo(fh: var File): (NDArrayDescriptor, bool, Shape) = proc readNumpy(fh: var File): BaseNDArray = var ((descrEndianness, descrType, descrSize), _, shape) = readPageInfo(fh) - var page: BaseNDArray - - case descrType: - of D_BOOLEAN: page = newBooleanNDArray(fh, shape) - of D_INT: page = newIntNDArray(fh, descrEndianness, descrSize, shape) - of D_FLOAT: page = newFloatNDArray(fh, descrEndianness, descrSize, shape) - of D_UNICODE: page = newUnicodeNDArray(fh, descrEndianness, descrSize, shape) - of D_OBJECT: page = newObjectNDArray(fh, descrEndianness, shape) - of D_DATE_DAYS: page = newDateArray_Days(fh, descrEndianness, shape) - of D_DATETIME_SECONDS: page = newDateTimeArray_Seconds(fh, descrEndianness, shape) - of D_DATETIME_MILISECONDS: page = newDateTimeArray_Miliseconds(fh, descrEndianness, shape) - of D_DATETIME_MICROSECONDS: page = newDateTimeArray_Microseconds(fh, descrEndianness, shape) - else: implement($descrType) + let page = ( + case descrType: + of D_BOOLEAN: newBooleanNDArray(fh, shape) + of D_INT: newIntNDArray(fh, descrEndianness, descrSize, shape) + of D_FLOAT: newFloatNDArray(fh, descrEndianness, descrSize, shape) + of D_UNICODE: newUnicodeNDArray(fh, descrEndianness, descrSize, shape) + of D_OBJECT: newObjectNDArray(fh, descrEndianness, shape) + of D_DATE_DAYS: newDateArray_Days(fh, descrEndianness, shape) + of D_DATETIME_SECONDS: newDateTimeArray_Seconds(fh, descrEndianness, shape) + of D_DATETIME_MILISECONDS: newDateTimeArray_Miliseconds(fh, descrEndianness, shape) + of D_DATETIME_MICROSECONDS: newDateTimeArray_Microseconds(fh, descrEndianness, shape) + of D_TIME_SECONDS: newTimeArray_Seconds(fh, descrEndianness, shape) + of D_TIME_MILISECONDS: newTimeArray_Miliseconds(fh, descrEndianness, shape) + of D_TIME_MICROSECONDS: newTimeArray_Microseconds(fh, descrEndianness, shape) + ) return page proc readNumpy*(path: string): BaseNDArray = diff --git a/master/objects.inv b/master/objects.inv index ec2db1bcd6734277b31985c5e816b874ae0e5bc3..2f27af20bf12da937f919c5fa9e6becdca3cbc7e 100644 GIT binary patch delta 444 zcmV;t0Ym=i9rPWrLk)lG(pD;2HP;*U)N<{ak@5~TwgV@fNi6U5xmsIRxmZe^H{n^u z?~psfqI17&=|0;@xmD|XNEe}C^iP~Gir74q~p3|%s39V@1{v? zA9YvXs7AOCG-gM-Ir4nZ$f6k;W1%Oa#v(GR+Tc;bUxDkei4FV?`gXpG!Y5a3Ce2C@ z(gpvGRm8aTVpM-+xX5(phwQ(-ZP!b#(w$Swux8X7Dxja%^m~L}YLDrV7suw=<^|Z3 zN*{G+G#olG(RWcSO}ZCAeNYy1etp~+L;IlXkNL+K&_ delta 432 zcmV;h0Z;z)9q1jfLk)jw)V@Pq>%d9p0L$BWuGSVrE|wDK9d=gnJLHaVxqJx27QyiR z@>eVlCp%^7?hyd+`=Qt{p!+iKFrNEAH1;wVI$*{grtvES~LlS?Y#^pD7L~Og2qsyLF zWgb`UIuS{iA_lB$yl}?LnW0WKijBehj9MK$>9{T#GmeApyJ-^JN8P10suAu3&Cro< zjy&HGvS>!eSm-IFv51VSHh7fq_uD#bV$-~XzMU_T@W~aMNwd;}bisdP6)`TF7*!cA zGTr$h`>$r(^^$+9bm!DEtQqx&3h1Xb{gR-U+G8T)#j*LMc>(sM(ub7VY{*%uO%2=W z84E|RTM~t!6|SV?ZXF|Ly+L_N3!-mz5n(mZF!p$94ckawT8DR-7lV`MhN>(_CsWKn z+5JLu+m$c+8b5+@X!4YI?ke5-c4+MFzA|16!|3OwHFYS`eq#IY>p2)Ns)n1bwf^m@ aL`@_5|Ldj_i|qCk(`v_j2>%CKtQNL$0NzOe diff --git a/master/reference/base/index.html b/master/reference/base/index.html index 3b3e2119..ae469777 100644 --- a/master/reference/base/index.html +++ b/master/reference/base/index.html @@ -561,7 +561,7 @@

  • -  base +  base @@ -2191,7 +2191,7 @@
  • -  base +  base diff --git a/master/reference/config/index.html b/master/reference/config/index.html index 66cb0258..5363db0b 100644 --- a/master/reference/config/index.html +++ b/master/reference/config/index.html @@ -582,7 +582,7 @@
  • -  config +  config @@ -1333,7 +1333,7 @@
  • -  config +  config diff --git a/master/reference/core/index.html b/master/reference/core/index.html index 1eec60bc..02c23832 100644 --- a/master/reference/core/index.html +++ b/master/reference/core/index.html @@ -603,7 +603,7 @@
  • -  core +  core @@ -1888,7 +1888,7 @@
  • -  core +  core diff --git a/master/reference/datasets/index.html b/master/reference/datasets/index.html index bbc322c9..6ee57aed 100644 --- a/master/reference/datasets/index.html +++ b/master/reference/datasets/index.html @@ -624,7 +624,7 @@
  • -  datasets +  datasets @@ -1153,7 +1153,7 @@
  • -  datasets +  datasets diff --git a/master/reference/datatypes/index.html b/master/reference/datatypes/index.html index c13e276a..07ddca3d 100644 --- a/master/reference/datatypes/index.html +++ b/master/reference/datatypes/index.html @@ -645,7 +645,7 @@
  • -  datatypes +  datatypes @@ -1798,7 +1798,7 @@
  • -  datatypes +  datatypes diff --git a/master/reference/diff/index.html b/master/reference/diff/index.html index 9cf2f00b..2b7be574 100644 --- a/master/reference/diff/index.html +++ b/master/reference/diff/index.html @@ -666,7 +666,7 @@
  • -  diff +  diff @@ -1153,7 +1153,7 @@
  • -  diff +  diff diff --git a/master/reference/export_utils/index.html b/master/reference/export_utils/index.html index 1f25ef37..999404ed 100644 --- a/master/reference/export_utils/index.html +++ b/master/reference/export_utils/index.html @@ -687,7 +687,7 @@
  • -  export_utils +  export_utils @@ -1249,7 +1249,7 @@
  • -  export_utils +  export_utils diff --git a/master/reference/file_reader_utils/index.html b/master/reference/file_reader_utils/index.html index 068e1783..d9c49bf8 100644 --- a/master/reference/file_reader_utils/index.html +++ b/master/reference/file_reader_utils/index.html @@ -708,7 +708,7 @@
  • -  file_reader_utils +  file_reader_utils @@ -1363,7 +1363,7 @@
  • -  file_reader_utils +  file_reader_utils diff --git a/master/reference/groupby_utils/index.html b/master/reference/groupby_utils/index.html index 4b0161c8..5639d08d 100644 --- a/master/reference/groupby_utils/index.html +++ b/master/reference/groupby_utils/index.html @@ -729,7 +729,7 @@
  • -  groupby_utils +  groupby_utils @@ -1273,7 +1273,7 @@
  • -  groupby_utils +  groupby_utils diff --git a/master/reference/import_utils/index.html b/master/reference/import_utils/index.html index db30a8f1..b0e5e2dc 100644 --- a/master/reference/import_utils/index.html +++ b/master/reference/import_utils/index.html @@ -750,7 +750,7 @@
  • -  import_utils +  import_utils @@ -1444,7 +1444,7 @@
  • -  import_utils +  import_utils diff --git a/master/reference/imputation/index.html b/master/reference/imputation/index.html index 910ebff7..d381a58b 100644 --- a/master/reference/imputation/index.html +++ b/master/reference/imputation/index.html @@ -771,7 +771,7 @@
  • -  imputation +  imputation @@ -1180,7 +1180,7 @@
  • -  imputation +  imputation diff --git a/master/reference/joins/index.html b/master/reference/joins/index.html index e3f5e84a..60a4d0cf 100644 --- a/master/reference/joins/index.html +++ b/master/reference/joins/index.html @@ -792,7 +792,7 @@
  • -  joins +  joins @@ -1189,7 +1189,7 @@
  • -  joins +  joins diff --git a/master/reference/lookup/index.html b/master/reference/lookup/index.html index 7aa67969..988d2c8b 100644 --- a/master/reference/lookup/index.html +++ b/master/reference/lookup/index.html @@ -813,7 +813,7 @@
  • -  lookup +  lookup @@ -1162,7 +1162,7 @@
  • -  lookup +  lookup diff --git a/master/reference/match/index.html b/master/reference/match/index.html index 73d3c14e..f73bf01f 100644 --- a/master/reference/match/index.html +++ b/master/reference/match/index.html @@ -834,7 +834,7 @@
  • -  match +  match @@ -1171,7 +1171,7 @@
  • -  match +  match diff --git a/master/reference/merge/index.html b/master/reference/merge/index.html index aeb4dc4a..80756dbd 100644 --- a/master/reference/merge/index.html +++ b/master/reference/merge/index.html @@ -855,7 +855,7 @@
  • -  merge +  merge @@ -1153,7 +1153,7 @@
  • -  merge +  merge diff --git a/master/reference/mp_utils/index.html b/master/reference/mp_utils/index.html index 13118d91..d5d114f9 100644 --- a/master/reference/mp_utils/index.html +++ b/master/reference/mp_utils/index.html @@ -876,7 +876,7 @@
  • -  mp_utils +  mp_utils @@ -1249,7 +1249,7 @@
  • -  mp_utils +  mp_utils diff --git a/master/reference/nimlite/index.html b/master/reference/nimlite/index.html index 7c725404..b31bc7eb 100644 --- a/master/reference/nimlite/index.html +++ b/master/reference/nimlite/index.html @@ -897,7 +897,7 @@
  • -  nimlite +  nimlite @@ -1321,7 +1321,7 @@
  • -  nimlite +  nimlite diff --git a/master/reference/pivots/index.html b/master/reference/pivots/index.html index c44e6568..c149e8dd 100644 --- a/master/reference/pivots/index.html +++ b/master/reference/pivots/index.html @@ -918,7 +918,7 @@
  • -  pivots +  pivots @@ -1171,7 +1171,7 @@
  • -  pivots +  pivots diff --git a/master/reference/redux/index.html b/master/reference/redux/index.html index 1d59f8a4..212beef4 100644 --- a/master/reference/redux/index.html +++ b/master/reference/redux/index.html @@ -939,7 +939,7 @@
  • -  redux +  redux @@ -999,6 +999,15 @@ +
  • + +
  • + + +  filter_non_primitive + + +
  • @@ -1189,7 +1198,7 @@
  • -  redux +  redux @@ -1249,6 +1258,15 @@ +
  • + +
  • + + +  filter_non_primitive + + +
  • @@ -1354,8 +1372,7 @@

    Source code in tablite/redux.py -
    33
    -34
    +            
    34
     35
     36
     37
    @@ -1405,7 +1422,8 @@ 

    81 82 83 -84

    def filter_all(T, **kwargs):
    +84
    +85
    def filter_all(T, **kwargs):
         """
         returns Table for rows where ALL kwargs match
         :param kwargs: dictionary with headers and values / boolean callable
    @@ -1507,8 +1525,7 @@ 

    Source code in tablite/redux.py -
     87
    - 88
    +            
     88
      89
      90
      91
    @@ -1523,7 +1540,8 @@ 

    100 101 102 -103

    def drop(T, *args):
    +103
    +104
    def drop(T, *args):
         """drops all rows that contain args
     
         Args:
    @@ -1564,8 +1582,7 @@ 

    Source code in tablite/redux.py -
    106
    -107
    +            
    107
     108
     109
     110
    @@ -1584,7 +1601,8 @@ 

    123 124 125 -126

    def filter_any(T, **kwargs):
    +126
    +127
    def filter_any(T, **kwargs):
         """
         returns Table for rows where ANY kwargs match
         :param kwargs: dictionary with headers and values / boolean callable
    @@ -1616,6 +1634,262 @@ 

    +

    + tablite.redux.filter_non_primitive(T, expressions, filter_type='all', tqdm=_tqdm) + +

    + + +
    + +

    OBSOLETE +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):
    +    return all((A==B, C!=4, 200<D))
    +
    +

    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 + +

    +
    + + + + + + + + + + + + + + + + +
    RETURNSDESCRIPTION
    + 2xTables + +
    +

    trues, falses

    +
    +
    + +
    + Source code in tablite/redux.py +
    288
    +289
    +290
    +291
    +292
    +293
    +294
    +295
    +296
    +297
    +298
    +299
    +300
    +301
    +302
    +303
    +304
    +305
    +306
    +307
    +308
    +309
    +310
    +311
    +312
    +313
    +314
    +315
    +316
    +317
    +318
    +319
    +320
    +321
    +322
    +323
    +324
    +325
    +326
    +327
    +328
    +329
    +330
    +331
    +332
    +333
    +334
    +335
    +336
    +337
    +338
    +339
    +340
    def filter_non_primitive(T, expressions, filter_type="all", tqdm=_tqdm):
    +    """
    +    OBSOLETE
    +    filters table
    +
    +
    +    Args:
    +        T (Table subclass): Table.
    +        expressions (list or str):
    +            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):
    +                    return all((A==B, C!=4, 200<D))
    +
    +            list of dicts: (example):
    +
    +            L = [
    +                {'column1':'A', 'criteria': "==", 'column2': 'B'},
    +                {'column1':'C', 'criteria': "!=", "value2": '4'},
    +                {'value1': 200, 'criteria': "<", column2: 'D' }
    +            ]
    +
    +        accepted dictionary keys: 'column1', 'column2', 'criteria', 'value1', 'value2'
    +
    +        filter_type (str, optional): Ignored if expressions is str.
    +            'all' or 'any'. Defaults to "all".
    +        tqdm (tqdm, optional): progressbar. Defaults to _tqdm.
    +
    +    Returns:
    +        2xTables: trues, falses
    +    """
    +    # determine method
    +    warnings.warn("Filter using non-primitive types is not recommended.")
    +    sub_cls_check(T, BaseTable)
    +    if len(T) == 0:
    +        return T.copy(), T.copy()
    +
    +    with tqdm(desc="filter", total=20) as pbar:
    +        if isinstance(expressions, str):
    +            mask = _filter_using_expression(T, expressions)
    +            pbar.update(10)
    +        elif isinstance(expressions, list):
    +            mask = _filter_using_list_of_dicts(T, expressions, filter_type, pbar)
    +        else:
    +            raise TypeError
    +        # create new tables
    +        res = _compress_both(T, mask, pbar=pbar)
    +        pbar.update(pbar.total - pbar.n)
    +
    +        return res
    +
    +
    +
    + +
    + + +
    + + +

    tablite.redux.filter(T, expressions, filter_type='all', tqdm=_tqdm) @@ -1624,7 +1898,8 @@

    -

    filters table

    +

    filters table +Note: At the moment only tablite primitive types are supported

    @@ -1755,59 +2030,59 @@

    Source code in tablite/redux.py -
    167
    -168
    -169
    -170
    -171
    -172
    -173
    -174
    -175
    -176
    -177
    -178
    -179
    -180
    -181
    -182
    -183
    -184
    -185
    -186
    -187
    -188
    -189
    -190
    -191
    -192
    -193
    -194
    -195
    -196
    -197
    -198
    -199
    -200
    -201
    -202
    -203
    -204
    -205
    -206
    -207
    -208
    -209
    -210
    -211
    -212
    -213
    -214
    -215
    -216
    -217
    def filter(T, expressions, filter_type="all", tqdm=_tqdm):
    +            
    342
    +343
    +344
    +345
    +346
    +347
    +348
    +349
    +350
    +351
    +352
    +353
    +354
    +355
    +356
    +357
    +358
    +359
    +360
    +361
    +362
    +363
    +364
    +365
    +366
    +367
    +368
    +369
    +370
    +371
    +372
    +373
    +374
    +375
    +376
    +377
    +378
    +379
    +380
    +381
    +382
    +383
    +384
    +385
    +386
    +387
    +388
    +389
    +390
    +391
    +392
    def filter(T, expressions, filter_type="all", tqdm=_tqdm):
         """filters table
    -
    +    Note: At the moment only tablite primitive types are supported
     
         Args:
             T (Table subclass): Table.
    @@ -1850,7 +2125,7 @@ 

    res = _compress_both(T, mask, pbar=pbar) pbar.update(pbar.total - pbar.n) elif isinstance(expressions, list): - return _filter_using_list_of_dicts(T, expressions, filter_type, tqdm) + return _filter_using_list_of_dicts_native(T, expressions, filter_type, tqdm) else: raise TypeError # create new tables diff --git a/master/reference/reindex/index.html b/master/reference/reindex/index.html index c3fc1db6..46a3c258 100644 --- a/master/reference/reindex/index.html +++ b/master/reference/reindex/index.html @@ -960,7 +960,7 @@
  • -  reindex +  reindex @@ -1153,7 +1153,7 @@
  • -  reindex +  reindex diff --git a/master/reference/sort_utils/index.html b/master/reference/sort_utils/index.html index 00325657..8b487f04 100644 --- a/master/reference/sort_utils/index.html +++ b/master/reference/sort_utils/index.html @@ -981,7 +981,7 @@
  • -  sort_utils +  sort_utils @@ -1330,7 +1330,7 @@
  • -  sort_utils +  sort_utils diff --git a/master/reference/sortation/index.html b/master/reference/sortation/index.html index d0e1e801..43e6e187 100644 --- a/master/reference/sortation/index.html +++ b/master/reference/sortation/index.html @@ -1002,7 +1002,7 @@
  • -  sortation +  sortation @@ -1189,7 +1189,7 @@
  • -  sortation +  sortation diff --git a/master/reference/tools/index.html b/master/reference/tools/index.html index ae308412..c128cbdc 100644 --- a/master/reference/tools/index.html +++ b/master/reference/tools/index.html @@ -1023,7 +1023,7 @@
  • -  tools +  tools @@ -1186,7 +1186,7 @@
  • -  tools +  tools diff --git a/master/reference/utils/index.html b/master/reference/utils/index.html index 14da0995..17d3e71a 100644 --- a/master/reference/utils/index.html +++ b/master/reference/utils/index.html @@ -1044,7 +1044,7 @@
  • -  utils +  utils @@ -1357,7 +1357,7 @@
  • -  utils +  utils diff --git a/master/reference/version/index.html b/master/reference/version/index.html index f7451a0b..986d63a5 100644 --- a/master/reference/version/index.html +++ b/master/reference/version/index.html @@ -1063,7 +1063,7 @@
  • -  version +  version @@ -1151,7 +1151,7 @@
  • -  version +  version diff --git a/master/search/search_index.json b/master/search/search_index.json index ad9a7e25..64828110 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
    • api
    • 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

    run: chmod +x ./build_nim.sh run: ./build_nim.sh

    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":"#api","title":"API","text":"

    To view the detailed API see api

    "},{"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":"
    • Eugene Antonov - the api documentation.
    • Audrius Kulikajevas - Edge case testing / various bugs, Jupyter notebook integration.
    • Ovidijus Grigas - various bugs, documentation.
    • Martynas Kaunas - GroupBy functionality.
    • Sergej Sinkarenko - various bugs.
    • 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/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.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.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 = f\"{os.getpid()}-{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.path)\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.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 = f\"{os.getpid()}-{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.path)\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 (defaults: Config.pid_dir)

    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 (defaults: Config.pid_dir)\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    from tablite.nimlite import repaginate as _repaginate\n\n    _repaginate(self)\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\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: Union[List[int], np.ndarray]) -> np.ndarray","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: Union[List[int], np.ndarray]) -> np.ndarray:\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, page in self.iter_by_page():\n        range_match = np.asarray(((indices >= start) & (indices < end)) | (indices == -1)).nonzero()[0]\n        if len(range_match):\n            # only fetch the data if there's a range match!\n            data = page.get() \n            sub_index = np.take(indices, range_match)\n            # sub_index2 otherwise will raise index error where len(data) > (-1 - start)\n            # so the clause below is required:\n            if len(data) > (-1 - start):\n                sub_index = np.where(sub_index == -1, -1, sub_index - start)\n            arr = np.take(data, sub_index)\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            py_dtype = page.dtype\n            for ix, v in enumerate(warray):\n                old_py_val = numpy_to_python(v)\n                new_py_val = mapping[old_py_val]\n                old_dt = type(old_py_val)\n                new_dt = type(new_py_val)\n\n                warray[ix] = new_py_val\n\n                py_dtype[new_dt] = py_dtype.get(new_dt, 0) + 1\n                py_dtype[old_dt] = py_dtype.get(old_dt, 0) - 1\n\n                if py_dtype[old_dt] <= 0:\n                    del py_dtype[old_dt]\n\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.BaseTable","title":"tablite.base.BaseTable(columns: [dict, None] = None, headers: [list, None] = None, rows: [list, None] = None, _path: [Path, None] = 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]})

    _path

    path to main process working directory.

    TYPE: Path DEFAULT: None

    Source code in tablite/base.py
    def __init__(\n    self,\n    columns: [dict, None] = None,\n    headers: [list, None] = None,\n    rows: [list, None] = None,\n    _path: [Path, None] = None,\n) -> 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        _path (pathlib.Path, optional): path to main process working directory.\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.BaseTable-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.BaseTable.path","title":"tablite.base.BaseTable.path = _path instance-attribute","text":""},{"location":"reference/base/#tablite.base.BaseTable.columns","title":"tablite.base.BaseTable.columns = {} instance-attribute","text":""},{"location":"reference/base/#tablite.base.BaseTable.rows","title":"tablite.base.BaseTable.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.BaseTable-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.BaseTable.__str__","title":"tablite.base.BaseTable.__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.BaseTable.__repr__","title":"tablite.base.BaseTable.__repr__()","text":"Source code in tablite/base.py
    def __repr__(self):\n    return self.__str__()\n
    "},{"location":"reference/base/#tablite.base.BaseTable.nbytes","title":"tablite.base.BaseTable.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.BaseTable.items","title":"tablite.base.BaseTable.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.BaseTable.__delitem__","title":"tablite.base.BaseTable.__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.BaseTable.__setitem__","title":"tablite.base.BaseTable.__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.BaseTable.__getitem__","title":"tablite.base.BaseTable.__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.BaseTable.__len__","title":"tablite.base.BaseTable.__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.BaseTable.__eq__","title":"tablite.base.BaseTable.__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, BaseTable):\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.BaseTable.clear","title":"tablite.base.BaseTable.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.BaseTable.save","title":"tablite.base.BaseTable.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    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path = path.parent / (path.parts[-1] + \".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.BaseTable.load","title":"tablite.base.BaseTable.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    path = 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.BaseTable.copy","title":"tablite.base.BaseTable.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.BaseTable.__imul__","title":"tablite.base.BaseTable.__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.BaseTable.__mul__","title":"tablite.base.BaseTable.__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.BaseTable.__iadd__","title":"tablite.base.BaseTable.__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, BaseTable)\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.BaseTable.__add__","title":"tablite.base.BaseTable.__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, BaseTable)\n    cp = self.copy()\n    cp += other\n    return cp\n
    "},{"location":"reference/base/#tablite.base.BaseTable.add_rows","title":"tablite.base.BaseTable.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 BaseTable._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        BaseTable._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.BaseTable.add_columns","title":"tablite.base.BaseTable.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.BaseTable.add_column","title":"tablite.base.BaseTable.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.BaseTable.stack","title":"tablite.base.BaseTable.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, BaseTable):\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.BaseTable.types","title":"tablite.base.BaseTable.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.BaseTable.display_dict","title":"tablite.base.BaseTable.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) == 0:\n            typ = \"empty\"\n        elif 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.BaseTable.to_ascii","title":"tablite.base.BaseTable.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.BaseTable.show","title":"tablite.base.BaseTable.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.BaseTable.to_dict","title":"tablite.base.BaseTable.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.BaseTable.as_json_serializable","title":"tablite.base.BaseTable.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.BaseTable.index","title":"tablite.base.BaseTable.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.BaseTable.unique_index","title":"tablite.base.BaseTable.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\"))\n

    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

    the new path will now be used for every new table.

    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":"

    10e9 (10Gb) on 100 Gb disk means raise at 90 Gb disk usage. if DISK_LIMIT <= 0, the check is turned off.

    "},{"location":"reference/config/#tablite.config.Config.SINGLE_PROCESSING_LIMIT","title":"tablite.config.Config.SINGLE_PROCESSING_LIMIT = 1000000 class-attribute instance-attribute","text":"

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

    "},{"location":"reference/config/#tablite.config.Config.vpus","title":"tablite.config.Config.vpus = max(os.cpu_count() - 1, 1) 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: BaseTable

    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, BaseTable):\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    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path = path.parent / (path.parts[-1] + \".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    path = 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, BaseTable)\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, BaseTable)\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 BaseTable._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        BaseTable._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, BaseTable):\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) == 0:\n            typ = \"empty\"\n        elif 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='', skip_empty: ValidSkipEmpty = 'NONE', 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    skip_empty: ValidSkipEmpty=\"NONE\",\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, skip_empty,\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            skip_empty,\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            skip_empty,\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 _groupby(self, keys, functions, tqdm)\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: list[ColumnSelectorDict], 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: list[ColumnSelectorDict], 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=None, right_columns=None, 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=None, right_columns=None, 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 x: date(*int(i) for i in x.split('-')), 'NNNN-N-NN': lambda x: date(*int(i) for i in x.split('-')), 'NNNN-NN-N': lambda x: date(*int(i) for i in x.split('-')), 'NNNN-N-N': lambda x: date(*int(i) for i in x.split('-')), 'NN-NN-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'N-NN-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'NN-N-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'N-N-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'NNNN.NN.NN': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.N.NN': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.NN.N': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.N.N': lambda x: date(*int(i) for i in x.split('.')), 'NN.NN.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'N.NN.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'NN.N.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'N.N.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'NNNN/NN/NN': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/N/NN': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/NN/N': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/N/N': lambda x: date(*int(i) for i in x.split('/')), 'NN/NN/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'N/NN/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'NN/N/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'N/N/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'NNNN NN NN': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN N NN': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN NN N': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN N N': lambda x: date(*int(i) for i in x.split(' ')), 'NN NN NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'N N NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'NN N NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'N NN NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'NNNNNNNN': lambda x: 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 x: DataTypes.pattern_to_datetime(x), 'NNNN-NN-NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x), 'NNNN-NN-NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, T=' '), 'NNNN-NN-NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, T=' '), 'NNNN/NN/NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN/NN/NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN NN NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN NN NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN.NN.NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NNNN.NN.NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NN-NN-NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN/NN/NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN/NN/NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN NN NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN.NN.NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NNNNNNNNTNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNTNN:NN:NN': lambda x: 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, BaseTable)\n    sub_cls_check(other, BaseTable)\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), BaseTable):\n        t1 = [tuple(r) for r in T.rows]\n    else:\n        t1 = list(T)\n    t2 = other[columns]\n    if issubclass(type(t2), BaseTable):\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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.header_readers","title":"tablite.file_reader_utils.header_readers = {'fods': excel_reader_headers, 'json': excel_reader_headers, 'simple': excel_reader_headers, 'rst': excel_reader_headers, 'mediawiki': excel_reader_headers, 'xlsx': excel_reader_headers, 'xlsm': excel_reader_headers, 'csv': text_reader_headers, 'tsv': text_reader_headers, 'txt': text_reader_headers, 'ods': ods_reader_headers} 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.text_reader_headers","title":"tablite.file_reader_utils.text_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def text_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {}\n    delimiters = {\n        \".csv\": \",\",\n        \".tsv\": \"\\t\",\n        \".txt\": None,\n    }\n\n    try:\n        with path.open(\"rb\") as fi:\n            rawdata = fi.read(ENCODING_GUESS_BYTES)\n            encoding = chardet.detect(rawdata)[\"encoding\"]\n\n        if delimiter is None:\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                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[path.name] = [lines]\n            d[\"is_empty\"] = True  # mark as empty to return an empty table instead of throwing\n        else:\n            kwargs = {}\n\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\n            d[path.name] = _get_headers(\n                str(path), py_to_nim_encoding(encoding), header_row_index=header_row_index,\n                delimiter=delimiter,\n                linecount=linecount,\n                **kwargs\n            )\n        return d\n    except Exception as e:\n        raise ValueError(f\"can't read {path.suffix}\")\n
    "},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.excel_reader_headers","title":"tablite.file_reader_utils.excel_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def excel_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {}\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            if sheet.max_row is None:\n                max_rows = 0\n            else:\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] = [None if c is None else c[0:padding_ends] for c in container]\n            d[\"delimiter\"] = None\n    finally:\n        book.close()\n\n    return d\n
    "},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.ods_reader_headers","title":"tablite.file_reader_utils.ods_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def ods_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {\n        \"delimiter\": None\n    }\n    sheets = pyexcel.get_book_dict(file_name=str(path))\n\n    for sheet_name, data in sheets.items():\n        lines = [[DataTypes.to_json(v) for v in row] for row in data[header_row_index:header_row_index+linecount]]\n\n        d[sheet_name] = lines\n\n    return d\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    kwargs = {\n        \"path\": path,\n        \"delimiter\": delimiter,\n        \"header_row_index\": header_row_index,\n        \"text_qualifier\": text_qualifier,\n        \"linecount\": linecount\n   }\n\n    reader = header_readers.get(path.suffix[1:], None)\n\n    if reader is None:\n        raise TypeError(f\"file format for headers not supported: {path.suffix}\")\n\n    result = reader(**kwargs)\n\n    return result\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.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/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, BaseTable):\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, BaseTable):\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, BaseTable):\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, BaseTable):\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, skip_empty='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, skip_empty=\"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, BaseTable):\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        \"\"\"\n            If no sheet specified, assume first sheet.\n\n            Reasoning:\n                Pandas ODS reader does that, so this preserves parity and it might be expected by users.\n                If we don't know the sheet name but only have single sheet,\n                    we would need to take extra steps to find out the name of the sheet.\n                We already make assumptions in case of column selection,\n                    when columns are None, we import all of them.\n        \"\"\"\n        sheet = book.sheetnames[0]\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        it_header = worksheet.iter_rows(min_row=header_row_index + 1)\n        while True:\n            # get the first row to know our headers or the number of columns\n            row = [c.value for c in next(it_header)]\n            break\n        fields = [str(c) if c is not None else \"\" for c in row] # 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 = iter(tqdm_iter)\n\n    idx = 0\n\n    while True:\n        try:\n            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 skip_empty == \"ALL\" and all(v is None for v in row):\n            continue\n        elif skip_empty == \"ANY\" and any(v is None for v in row):\n            continue\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        idx = idx + 1\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, skip_empty='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, skip_empty=\"NONE\", start=0, limit=sys.maxsize, **kwargs):\n    \"\"\"\n    returns Table from .ODS\n    \"\"\"\n    if not issubclass(T, BaseTable):\n        raise TypeError(\"Expected subclass of Table\")\n\n    if sheet is None:\n        data = read_excel(str(path), header=None) # selects first sheet\n    else:\n        data = read_excel(str(path), sheet_name=sheet, header=None)\n\n    data[isna(data)] = None  # convert any empty cells to None\n    data = data.to_numpy().tolist() # convert pandas to list\n\n    if skip_empty == \"ALL\" or skip_empty == \"ANY\":\n        \"\"\" filter out all rows based on predicate that come after header row \"\"\"\n        fn_filter = any if skip_empty == \"ALL\" else all # this is intentional\n        data = [\n            row\n            for ridx, row in enumerate(data)\n            if ridx < header_row_index + (1 if first_row_has_headers else 0) or fn_filter(not (v is None or isinstance(v, str) and len(v) == 0) for v in row)\n        ]\n\n    data = np.array(data, dtype=np.object_) # cast back to numpy array for slicing but don't try to convert datatypes\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 = \"\" if value is None else 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        column_values = data[start_row_pos : start_row_pos + limit, ix]\n\n        t[unique_column_name] = column_values\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, skip_empty, 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    skip_empty,\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    enc = py_to_nim_encoding(encoding)\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    if skip_empty is None:\n        kwargs[\"skip_empty\"] = \"NONE\"\n    else:\n        kwargs[\"skip_empty\"] = skip_empty\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, BaseTable)\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=pbar)\n    elif method in {\"mean\", \"mode\"}:\n        return stats_method(T, targets, missing, method, tqdm=tqdm, pbar=pbar)\n    elif method == \"nearest neighbour\":\n        return nearest_neighbour(T, sources, missing, targets, tqdm=tqdm)\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 = T.copy()\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 = T.copy()\n    for name in T.columns:\n        if name in targets:\n            col = T.columns[name]\n            assert isinstance(col, Column)\n\n            hist_values, hist_counts = col.histogram()\n\n            for m in missing:\n                try:\n                    idx = hist_values.index(m)\n                    hist_counts[idx] = 0\n                except ValueError:\n                    pass\n\n            stats = summary_statistics(hist_values, hist_counts)\n\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-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: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], kind: str = 'inner', merge_keys: bool = False, 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.

    Example: \"inner\"

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

    Tablite:

    >>> inner_join = numbers.inner_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n)\n

    Example: \"left\"

    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

    Example: \"outer\"

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

    Tablite:

    >>> outer_join = numbers.outer_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n    )\n

    Example: \"cross\"

    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 join(\n    T: BaseTable,\n    other: BaseTable,\n    left_keys: List[str],\n    right_keys: List[str],\n    left_columns: Union[List[str], None],\n    right_columns: Union[List[str], None],\n    kind: str = \"inner\",\n    merge_keys: bool = False,\n    tqdm=_tqdm,\n    pbar=None,\n):\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    Example: \"inner\"\n    ```\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    ```\n    Tablite: \n    ```\n    >>> inner_join = numbers.inner_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n    )\n    ```\n\n    Example: \"left\" \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    Example: \"outer\"\n    ```\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    ```\n\n    Tablite: \n    ```\n    >>> outer_join = numbers.outer_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n        )\n    ```\n\n    Example: \"cross\"\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}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    return _join(kind, T,other,left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys,\n             tqdm=tqdm, pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.inner_join","title":"tablite.joins.inner_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def inner_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.left_join","title":"tablite.joins.left_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def left_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"left\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.outer_join","title":"tablite.joins.outer_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def outer_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"outer\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.cross_join","title":"tablite.joins.cross_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def cross_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"cross\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\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, BaseTable)\n    sub_cls_check(other, BaseTable)\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    Constr = type(T)\n    if isinstance(left, Column):\n        tmp, left = left, Constr()\n        left[left_columns[0]] = tmp\n    right = other[right_columns]\n    if isinstance(right, Column):\n        tmp, right = right, Constr()\n        right[right_columns[0]] = tmp\n    assert isinstance(left, BaseTable)\n    assert isinstance(right, BaseTable)\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, BaseTable)\n    assert isinstance(other, BaseTable)\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 and creates a single new column.

    :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    and creates a single new column.\n\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, BaseTable)\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_uq = unique_name(new, list(T.columns))\n    T.add_column(new_uq)\n    col = T[new_uq]\n\n    for start,end in Config.page_steps(len(criteria)):\n        left_values = T[left][start:end]\n        right_values = T[right][start:end]\n        new_values = np.where(criteria, left_values, right_values)\n        col.extend(new_values)\n\n    if new == right:\n        T[right] = T[new_uq]  # keep column order\n        del T[new_uq]\n        del T[left]\n    elif new == left:\n        T[left] = T[new_uq]  # keep column order\n        del T[new_uq]\n        del T[right]\n    else:\n        T[new] = T[new_uq]\n        del T[left]\n        del T[right]\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.is_mp","title":"tablite.mp_utils.is_mp(fields: int) -> bool","text":"PARAMETER DESCRIPTION fields

    number of fields

    TYPE: int

    RETURNS DESCRIPTION bool

    bool

    Source code in tablite/mp_utils.py
    def is_mp(fields: int) -> bool:\n    \"\"\"\n\n    Args:\n        fields (int): number of fields\n\n    Returns:\n        bool\n    \"\"\"\n    if Config.MULTIPROCESSING_MODE == Config.FORCE:\n        return True\n\n    if Config.MULTIPROCESSING_MODE == Config.FALSE:\n        return False\n\n    if fields < Config.SINGLE_PROCESSING_LIMIT:\n        return False\n\n    if max(psutil.cpu_count(logical=False), 1) < 2:\n        return False\n\n    return True\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    return mp if is_mp(fields) else sp\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.paths","title":"tablite.nimlite.paths = sys.argv[:] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.K","title":"tablite.nimlite.K = TypeVar('K', bound=BaseTable) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidEncoders","title":"tablite.nimlite.ValidEncoders = Literal['ENC_UTF8', 'ENC_UTF16', 'ENC_WIN1250'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidQuoting","title":"tablite.nimlite.ValidQuoting = Literal['QUOTE_MINIMAL', 'QUOTE_ALL', 'QUOTE_NONNUMERIC', 'QUOTE_NONE', 'QUOTE_STRINGS', 'QUOTE_NOTNULL'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidSkipEmpty","title":"tablite.nimlite.ValidSkipEmpty = Literal['NONE', 'ANY', 'ALL'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ColumnSelectorDict","title":"tablite.nimlite.ColumnSelectorDict = TypedDict('ColumnSelectorDict', {'column': str, 'type': Literal['int', 'float', 'bool', 'str', 'date', 'time', 'datetime'], 'allow_empty': Union[bool, None], 'rename': Union[str, None]}) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterCriteria","title":"tablite.nimlite.FilterCriteria = Literal['>', '>=', '==', '<', '<=', '!=', 'in'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterType","title":"tablite.nimlite.FilterType = Literal['all', 'any'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterDict","title":"tablite.nimlite.FilterDict = TypedDict('FilterDict', {'column1': str, 'value1': Union[str, None], 'criteria': FilterCriteria, 'column2': str, 'value2': Union[str, None]}) 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.get_headers","title":"tablite.nimlite.get_headers(path: Union[str, Path], encoding: ValidEncoders = 'ENC_UTF8', *, header_row_index: int = 0, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool = True, linecount: int = 10) -> list[list[str]]","text":"Source code in tablite/nimlite.py
    def get_headers(\n    path: Union[str, Path],\n    encoding: ValidEncoders =\"ENC_UTF8\",\n    *,\n    header_row_index: int=0,\n    newline: str='\\n', delimiter: str=',', text_qualifier: str='\"',\n    quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool=True,\n    linecount: int = 10\n) -> list[list[str]]:\n    return nl.get_headers(\n            path=str(path),\n            encoding=encoding,\n            newline=newline, delimiter=delimiter, text_qualifier=text_qualifier,\n            strip_leading_and_tailing_whitespace=strip_leading_and_tailing_whitespace,\n            header_row_index=header_row_index,\n            quoting=quoting,\n            linecount=linecount\n        )\n
    "},{"location":"reference/nimlite/#tablite.nimlite.text_reader","title":"tablite.nimlite.text_reader(T: Type[K], pid: str, path: Union[str, Path], encoding: ValidEncoders = 'ENC_UTF8', *, first_row_has_headers: bool = True, header_row_index: int = 0, columns: List[Union[str, None]] = None, start: Union[str, None] = None, limit: Union[str, None] = None, guess_datatypes: bool = False, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool = True, skip_empty: ValidSkipEmpty = 'NONE', tqdm=_tqdm) -> K","text":"Source code in tablite/nimlite.py
    def text_reader(\n    T: Type[K],\n    pid: str, path: Union[str, Path],\n    encoding: ValidEncoders =\"ENC_UTF8\",\n    *,\n    first_row_has_headers: bool=True, header_row_index: int=0,\n    columns: List[Union[str, None]]=None,\n    start: Union[str, None] = None, limit: Union[str, None]=None,\n    guess_datatypes: bool =False,\n    newline: str='\\n', delimiter: str=',', text_qualifier: str='\"',\n    quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool=True, skip_empty: ValidSkipEmpty = \"NONE\",\n    tqdm=_tqdm\n) -> K:\n    assert isinstance(path, Path)\n    assert isinstance(pid, Path)\n    with tqdm(total=10, desc=f\"importing file\") as pbar:\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            skip_empty=skip_empty,\n            page_size=Config.PAGE_SIZE\n        )\n\n        pbar.update(1)\n\n        task_info = table[\"task\"]\n        task_columns = table[\"columns\"]\n\n        ti_tasks = task_info[\"tasks\"]\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        pbar_step = 4 / max(len(ti_tasks), 1)\n\n        class WrapUpdate:\n            def update(self, n):\n                pbar.update(n * pbar_step)\n\n        wrapped_pbar = WrapUpdate()\n\n        def next_task(task: Task, page_info):\n            wrapped_pbar.update(1)\n            return Task(\n                nl.text_reader_task,\n                *task.args, **task.kwargs, page_info=page_info\n            )\n\n        tasks = [\n            TaskChain(\n                Task(\n                    nl.collect_text_reader_page_info_task,\n                    task=t,\n                    task_info=task_info\n                ), next_task=next_task\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.FORCE:\n            is_sp = False\n        elif Config.MULTIPROCESSING_MODE == Config.AUTO and cpus <= 1 or len(tasks) <= 1:\n            is_sp = True\n\n        if is_sp:\n            res = []\n\n            for task in tasks:\n                page = task.execute()\n\n                res.append(page)\n        else:\n            with TaskManager(cpus, error_mode=\"exception\") as tm:\n                res = tm.execute(tasks, pbar=wrapped_pbar)\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        pbar.update(pbar.total - pbar.n)\n\n        table = T(columns=table_dict)\n\n    return table\n
    "},{"location":"reference/nimlite/#tablite.nimlite.wrap","title":"tablite.nimlite.wrap(str_: str) -> str","text":"Source code in tablite/nimlite.py
    def wrap(str_: str) -> str:\n    return '\"' + str_.replace('\"', '\\\\\"').replace(\"'\", \"\\\\'\").replace(\"\\n\", \"\\\\n\").replace(\"\\t\", \"\\\\t\") + '\"'\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: K, cols: list[ColumnSelectorDict], tqdm=_tqdm, TaskManager=TaskManager) -> Tuple[K, K]:\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        col_infos = nl.collect_column_select_info(table, cols, str(dir_pid), pbar)\n\n        columns = col_infos[\"columns\"]\n        page_count = col_infos[\"page_count\"]\n        is_correct_type = col_infos[\"is_correct_type\"]\n        desired_column_map = col_infos[\"desired_column_map\"]\n        original_pages_map = col_infos[\"original_pages_map\"]\n        passed_column_data = col_infos[\"passed_column_data\"]\n        failed_column_data = col_infos[\"failed_column_data\"]\n        res_cols_pass = col_infos[\"res_cols_pass\"]\n        res_cols_fail = col_infos[\"res_cols_fail\"]\n        column_names = col_infos[\"column_names\"]\n        reject_reason_name = col_infos[\"reject_reason_name\"]\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, original_pages_map)\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)\n\n        if is_mp:\n            class WrapUpdate:\n                def update(self, n):\n                    pbar.update(n * step_size)\n\n            with TaskManager(min(cpu_count, page_count), error_mode=\"exception\") as tm:\n                res = tm.execute(list(tasks), pbar=WrapUpdate())\n\n                converted.extend(res)\n        else:\n            for task in tasks:\n                res = task.f(*task.args, **task.kwargs)\n\n                converted.append(res)\n                pbar.update(step_size)\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: Union[str, Path]) -> np.ndarray","text":"Source code in tablite/nimlite.py
    def read_page(path: Union[str, Path]) -> np.ndarray:\n    return nl.read_page(str(path))\n
    "},{"location":"reference/nimlite/#tablite.nimlite.repaginate","title":"tablite.nimlite.repaginate(column: Column)","text":"Source code in tablite/nimlite.py
    def repaginate(column: Column):\n    nl.repaginate(column)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.nearest_neighbour","title":"tablite.nimlite.nearest_neighbour(T: BaseTable, sources: Union[list[str], None], missing: Union[list, None], targets: Union[list[str], None], tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def nearest_neighbour(T: BaseTable, sources: Union[list[str], None], missing: Union[list, None], targets: Union[list[str], None], tqdm=_tqdm):\n    return nl.nearest_neighbour(T, sources, list(missing), targets, tqdm)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.groupby","title":"tablite.nimlite.groupby(T, keys, functions, tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def groupby(T, keys, functions, tqdm=_tqdm):\n    return nl.groupby(T, keys, functions, tqdm)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.filter","title":"tablite.nimlite.filter(table: BaseTable, expressions: list[FilterDict], type: FilterType, tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def filter(table: BaseTable, expressions: list[FilterDict], type: FilterType, tqdm = _tqdm):\n    return nl.filter(table, expressions, type, tqdm)\n
    "},{"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, BaseTable)\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)\n    Constr = type(T)\n\n    if len(grpby) == 0:  # return empty table. This must be a test?\n        pbar.update(extra_steps)\n        return Constr()\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}({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}({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, BaseTable)\n\n    if len(T.columns) == 0:\n        return type(T)()\n\n    assert isinstance(T, BaseTable)\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, BaseTable)\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        it = T[keep + columns].rows if len(keep + columns) > 1 else ((v, ) for v in T[keep + columns])\n\n        for ix, row in enumerate(it, 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, BaseTable)\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, page in col.iter_by_page():\n            data = page.get()\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, BaseTable)\n    mask = np.full((len(T),), False)\n    for name in T.columns:\n        col = T[name]\n        for start, end, page in col.iter_by_page():\n            data = page.get()\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, BaseTable)\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, page in col.iter_by_page():\n            data = page.get()\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, BaseTable)\n    if len(T) == 0:\n        return T.copy(), T.copy()\n\n    if isinstance(expressions, str):\n        with tqdm(desc=\"filter\", total=20) as pbar:\n            # TODO: make parser for expressions and use the nim implement\n            mask = _filter_using_expression(T, expressions)\n            pbar.update(10)\n            res = _compress_both(T, mask, pbar=pbar)\n            pbar.update(pbar.total - pbar.n)\n    elif isinstance(expressions, list):\n        return _filter_using_list_of_dicts(T, expressions, filter_type, tqdm)\n    else:\n        raise TypeError\n        # create new tables\n\n    return res\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, BaseTable)\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-classes","title":"Classes","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict","title":"tablite.sort_utils.HashDict","text":"

    Bases: dict

    This class is just a nicity syntatic sugar for debugging. Function identically to regular dictionary, just uses tupled key.

    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict-functions","title":"Functions","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.items","title":"tablite.sort_utils.HashDict.items()","text":"Source code in tablite/sort_utils.py
    def items(self):\n    return [(k, v) for (_, k), v in super().items()]\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.keys","title":"tablite.sort_utils.HashDict.keys()","text":"Source code in tablite/sort_utils.py
    def keys(self):\n    return [k for (_, k) in super().keys()]\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__iter__","title":"tablite.sort_utils.HashDict.__iter__() -> Iterator","text":"Source code in tablite/sort_utils.py
    def __iter__(self) -> Iterator:\n    return (k for (_, k) in super().keys())\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__getitem__","title":"tablite.sort_utils.HashDict.__getitem__(key)","text":"Source code in tablite/sort_utils.py
    def __getitem__(self, key):\n    return super().__getitem__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__setitem__","title":"tablite.sort_utils.HashDict.__setitem__(key, value)","text":"Source code in tablite/sort_utils.py
    def __setitem__(self, key, value):\n    return super().__setitem__(self._get_hash(key), value)\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__contains__","title":"tablite.sort_utils.HashDict.__contains__(key) -> bool","text":"Source code in tablite/sort_utils.py
    def __contains__(self, key) -> bool:\n    return super().__contains__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__delitem__","title":"tablite.sort_utils.HashDict.__delitem__(key)","text":"Source code in tablite/sort_utils.py
    def __delitem__(self, key):\n    return super().__delitem__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__repr__","title":"tablite.sort_utils.HashDict.__repr__() -> str","text":"Source code in tablite/sort_utils.py
    def __repr__(self) -> str:\n    return '{' + \", \".join([f\"{k}: {v}\" for k, v in self.items()]) + '}'\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__str__","title":"tablite.sort_utils.HashDict.__str__() -> str","text":"Source code in tablite/sort_utils.py
    def __str__(self) -> str:\n    return repr(self)\n
    "},{"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    d = HashDict()\n    L = non_text + text\n    for ix, (_, _, value) in enumerate(L):\n        d[value] = ix\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, BaseTable)\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, BaseTable)\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, BaseTable)\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.NoneType","title":"tablite.utils.NoneType = type(None) 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-classes","title":"Classes","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    max_col, max_row = 0, 0\n\n    regex = re.compile(\"\\d+\")\n\n    def handleStartElement(name, attrs):\n        nonlocal max_col, max_row\n\n        if name == \"c\":\n            last_index = attrs[\"r\"]\n            idx, _ = next(regex.finditer(last_index)).span()\n            letters, digits = last_index[0:idx], int(last_index[idx:])\n\n            col_idx, row_idx = calc_col_count(letters), digits\n\n            max_col, max_row = max(max_col, col_idx), max(max_row, row_idx)\n\n    parser = expat.ParserCreate()\n    parser.buffer_text = True\n    parser.StartElementHandler = handleStartElement\n    parser.ParseFile(src)\n\n    return max_col, max_row\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/utils/#tablite.utils.select_type_name","title":"tablite.utils.select_type_name(dtypes: dict)","text":"Source code in tablite/utils.py
    def select_type_name(dtypes: dict):\n    dtypes = [t for t in dtypes.items() if t[0] != NoneType]\n\n    if len(dtypes) == 0:\n        return \"empty\"\n\n    (best_type, _), *_ = sorted(dtypes, key=lambda t: t[1], reverse=True)\n\n    return best_type.__name__\n
    "},{"location":"reference/utils/#tablite.utils.get_predominant_types","title":"tablite.utils.get_predominant_types(table, all_dtypes=None)","text":"Source code in tablite/utils.py
    def get_predominant_types(table, all_dtypes=None):\n    if all_dtypes is None:\n        all_dtypes = table.types()\n\n    dtypes = {\n        k: select_type_name(v)\n        for k, v in all_dtypes.items()\n    }\n\n    return dtypes\n
    "},{"location":"reference/utils/#tablite.utils.py_to_nim_encoding","title":"tablite.utils.py_to_nim_encoding(encoding: str) -> str","text":"Source code in tablite/utils.py
    def py_to_nim_encoding(encoding: str) -> str:\n    if encoding is None or encoding.lower() in [\"ascii\", \"utf8\", \"utf-8\", \"utf-8-sig\"]:\n        return \"ENC_UTF8\"\n    elif encoding.lower() in [\"utf16\", \"utf-16\"]:\n        return \"ENC_UTF16\"\n    elif encoding in Config.NIM_SUPPORTED_CONV_TYPES:\n        return f\"ENC_CONV|{encoding}\"\n\n    raise NotImplementedError(f\"encoding not implemented: {encoding}\")\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
    • api
    • 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

    run: chmod +x ./build_nim.sh run: ./build_nim.sh

    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":"#api","title":"API","text":"

    To view the detailed API see api

    "},{"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":"
    • Eugene Antonov - the api documentation.
    • Audrius Kulikajevas - Edge case testing / various bugs, Jupyter notebook integration.
    • Ovidijus Grigas - various bugs, documentation.
    • Martynas Kaunas - GroupBy functionality.
    • Sergej Sinkarenko - various bugs.
    • 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/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.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.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 = f\"{os.getpid()}-{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.path)\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.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 = f\"{os.getpid()}-{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.path)\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 (defaults: Config.pid_dir)

    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 (defaults: Config.pid_dir)\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    from tablite.nimlite import repaginate as _repaginate\n\n    _repaginate(self)\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\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: Union[List[int], np.ndarray]) -> np.ndarray","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: Union[List[int], np.ndarray]) -> np.ndarray:\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, page in self.iter_by_page():\n        range_match = np.asarray(((indices >= start) & (indices < end)) | (indices == -1)).nonzero()[0]\n        if len(range_match):\n            # only fetch the data if there's a range match!\n            data = page.get() \n            sub_index = np.take(indices, range_match)\n            # sub_index2 otherwise will raise index error where len(data) > (-1 - start)\n            # so the clause below is required:\n            if len(data) > (-1 - start):\n                sub_index = np.where(sub_index == -1, -1, sub_index - start)\n            arr = np.take(data, sub_index)\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            py_dtype = page.dtype\n            for ix, v in enumerate(warray):\n                old_py_val = numpy_to_python(v)\n                new_py_val = mapping[old_py_val]\n                old_dt = type(old_py_val)\n                new_dt = type(new_py_val)\n\n                warray[ix] = new_py_val\n\n                py_dtype[new_dt] = py_dtype.get(new_dt, 0) + 1\n                py_dtype[old_dt] = py_dtype.get(old_dt, 0) - 1\n\n                if py_dtype[old_dt] <= 0:\n                    del py_dtype[old_dt]\n\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.BaseTable","title":"tablite.base.BaseTable(columns: [dict, None] = None, headers: [list, None] = None, rows: [list, None] = None, _path: [Path, None] = 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]})

    _path

    path to main process working directory.

    TYPE: Path DEFAULT: None

    Source code in tablite/base.py
    def __init__(\n    self,\n    columns: [dict, None] = None,\n    headers: [list, None] = None,\n    rows: [list, None] = None,\n    _path: [Path, None] = None,\n) -> 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        _path (pathlib.Path, optional): path to main process working directory.\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.BaseTable-attributes","title":"Attributes","text":""},{"location":"reference/base/#tablite.base.BaseTable.path","title":"tablite.base.BaseTable.path = _path instance-attribute","text":""},{"location":"reference/base/#tablite.base.BaseTable.columns","title":"tablite.base.BaseTable.columns = {} instance-attribute","text":""},{"location":"reference/base/#tablite.base.BaseTable.rows","title":"tablite.base.BaseTable.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.BaseTable-functions","title":"Functions","text":""},{"location":"reference/base/#tablite.base.BaseTable.__str__","title":"tablite.base.BaseTable.__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.BaseTable.__repr__","title":"tablite.base.BaseTable.__repr__()","text":"Source code in tablite/base.py
    def __repr__(self):\n    return self.__str__()\n
    "},{"location":"reference/base/#tablite.base.BaseTable.nbytes","title":"tablite.base.BaseTable.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.BaseTable.items","title":"tablite.base.BaseTable.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.BaseTable.__delitem__","title":"tablite.base.BaseTable.__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.BaseTable.__setitem__","title":"tablite.base.BaseTable.__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.BaseTable.__getitem__","title":"tablite.base.BaseTable.__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.BaseTable.__len__","title":"tablite.base.BaseTable.__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.BaseTable.__eq__","title":"tablite.base.BaseTable.__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, BaseTable):\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.BaseTable.clear","title":"tablite.base.BaseTable.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.BaseTable.save","title":"tablite.base.BaseTable.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    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path = path.parent / (path.parts[-1] + \".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.BaseTable.load","title":"tablite.base.BaseTable.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    path = 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.BaseTable.copy","title":"tablite.base.BaseTable.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.BaseTable.__imul__","title":"tablite.base.BaseTable.__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.BaseTable.__mul__","title":"tablite.base.BaseTable.__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.BaseTable.__iadd__","title":"tablite.base.BaseTable.__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, BaseTable)\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.BaseTable.__add__","title":"tablite.base.BaseTable.__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, BaseTable)\n    cp = self.copy()\n    cp += other\n    return cp\n
    "},{"location":"reference/base/#tablite.base.BaseTable.add_rows","title":"tablite.base.BaseTable.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 BaseTable._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        BaseTable._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.BaseTable.add_columns","title":"tablite.base.BaseTable.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.BaseTable.add_column","title":"tablite.base.BaseTable.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.BaseTable.stack","title":"tablite.base.BaseTable.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, BaseTable):\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.BaseTable.types","title":"tablite.base.BaseTable.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.BaseTable.display_dict","title":"tablite.base.BaseTable.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) == 0:\n            typ = \"empty\"\n        elif 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.BaseTable.to_ascii","title":"tablite.base.BaseTable.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.BaseTable.show","title":"tablite.base.BaseTable.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.BaseTable.to_dict","title":"tablite.base.BaseTable.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.BaseTable.as_json_serializable","title":"tablite.base.BaseTable.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.BaseTable.index","title":"tablite.base.BaseTable.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.BaseTable.unique_index","title":"tablite.base.BaseTable.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\"))\n

    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

    the new path will now be used for every new table.

    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":"

    10e9 (10Gb) on 100 Gb disk means raise at 90 Gb disk usage. if DISK_LIMIT <= 0, the check is turned off.

    "},{"location":"reference/config/#tablite.config.Config.SINGLE_PROCESSING_LIMIT","title":"tablite.config.Config.SINGLE_PROCESSING_LIMIT = 1000000 class-attribute instance-attribute","text":"

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

    "},{"location":"reference/config/#tablite.config.Config.vpus","title":"tablite.config.Config.vpus = max(os.cpu_count() - 1, 1) 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: BaseTable

    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, BaseTable):\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    if isinstance(path, str):\n        path = Path(path)\n    type_check(path, Path)\n    if path.is_dir():\n        raise TypeError(f\"filename needed: {path}\")\n    if path.suffix != \".tpz\":\n        path = path.parent / (path.parts[-1] + \".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    path = 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, BaseTable)\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, BaseTable)\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 BaseTable._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        BaseTable._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, BaseTable):\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) == 0:\n            typ = \"empty\"\n        elif 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='', skip_empty: ValidSkipEmpty = 'NONE', 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    skip_empty: ValidSkipEmpty=\"NONE\",\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, skip_empty,\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            skip_empty,\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            skip_empty,\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 _groupby(self, keys, functions, tqdm)\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: list[ColumnSelectorDict], 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: list[ColumnSelectorDict], 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=None, right_columns=None, 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=None, right_columns=None, 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 x: date(*int(i) for i in x.split('-')), 'NNNN-N-NN': lambda x: date(*int(i) for i in x.split('-')), 'NNNN-NN-N': lambda x: date(*int(i) for i in x.split('-')), 'NNNN-N-N': lambda x: date(*int(i) for i in x.split('-')), 'NN-NN-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'N-NN-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'NN-N-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'N-N-NNNN': lambda x: date(*[int(i) for i in x.split('-')][::-1]), 'NNNN.NN.NN': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.N.NN': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.NN.N': lambda x: date(*int(i) for i in x.split('.')), 'NNNN.N.N': lambda x: date(*int(i) for i in x.split('.')), 'NN.NN.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'N.NN.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'NN.N.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'N.N.NNNN': lambda x: date(*[int(i) for i in x.split('.')][::-1]), 'NNNN/NN/NN': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/N/NN': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/NN/N': lambda x: date(*int(i) for i in x.split('/')), 'NNNN/N/N': lambda x: date(*int(i) for i in x.split('/')), 'NN/NN/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'N/NN/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'NN/N/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'N/N/NNNN': lambda x: date(*[int(i) for i in x.split('/')][::-1]), 'NNNN NN NN': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN N NN': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN NN N': lambda x: date(*int(i) for i in x.split(' ')), 'NNNN N N': lambda x: date(*int(i) for i in x.split(' ')), 'NN NN NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'N N NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'NN N NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'N NN NNNN': lambda x: date(*[int(i) for i in x.split(' ')][::-1]), 'NNNNNNNN': lambda x: 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 x: DataTypes.pattern_to_datetime(x), 'NNNN-NN-NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x), 'NNNN-NN-NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, T=' '), 'NNNN-NN-NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, T=' '), 'NNNN/NN/NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/'), 'NNNN/NN/NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN/NN/NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' '), 'NNNN NN NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' '), 'NNNN NN NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN NN NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd=' ', T=' '), 'NNNN.NN.NNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.'), 'NNNN.NN.NN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NNNN.NN.NN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', T=' '), 'NN-NN-NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN-NN-NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='-', T=' ', day_first=True), 'NN/NN/NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN/NN/NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN/NN/NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', T=' ', day_first=True), 'NN NN NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN NN NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='/', day_first=True), 'NN.NN.NNNNTNN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNNTNN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NN.NN.NNNN NN:NN': lambda x: DataTypes.pattern_to_datetime(x, ymd='.', day_first=True), 'NNNNNNNNTNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNTNN': lambda x: DataTypes.pattern_to_datetime(x, compact=1), 'NNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNNNNNNN': lambda x: DataTypes.pattern_to_datetime(x, compact=2), 'NNNNNNNNTNN:NN:NN': lambda x: 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, BaseTable)\n    sub_cls_check(other, BaseTable)\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), BaseTable):\n        t1 = [tuple(r) for r in T.rows]\n    else:\n        t1 = list(T)\n    t2 = other[columns]\n    if issubclass(type(t2), BaseTable):\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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, BaseTable)\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.header_readers","title":"tablite.file_reader_utils.header_readers = {'fods': excel_reader_headers, 'json': excel_reader_headers, 'simple': excel_reader_headers, 'rst': excel_reader_headers, 'mediawiki': excel_reader_headers, 'xlsx': excel_reader_headers, 'xlsm': excel_reader_headers, 'csv': text_reader_headers, 'tsv': text_reader_headers, 'txt': text_reader_headers, 'ods': ods_reader_headers} 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.text_reader_headers","title":"tablite.file_reader_utils.text_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def text_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {}\n    delimiters = {\n        \".csv\": \",\",\n        \".tsv\": \"\\t\",\n        \".txt\": None,\n    }\n\n    try:\n        with path.open(\"rb\") as fi:\n            rawdata = fi.read(ENCODING_GUESS_BYTES)\n            encoding = chardet.detect(rawdata)[\"encoding\"]\n\n        if delimiter is None:\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                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[path.name] = [lines]\n            d[\"is_empty\"] = True  # mark as empty to return an empty table instead of throwing\n        else:\n            kwargs = {}\n\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\n            d[path.name] = _get_headers(\n                str(path), py_to_nim_encoding(encoding), header_row_index=header_row_index,\n                delimiter=delimiter,\n                linecount=linecount,\n                **kwargs\n            )\n        return d\n    except Exception as e:\n        raise ValueError(f\"can't read {path.suffix}\")\n
    "},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.excel_reader_headers","title":"tablite.file_reader_utils.excel_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def excel_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {}\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            if sheet.max_row is None:\n                max_rows = 0\n            else:\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] = [None if c is None else c[0:padding_ends] for c in container]\n            d[\"delimiter\"] = None\n    finally:\n        book.close()\n\n    return d\n
    "},{"location":"reference/file_reader_utils/#tablite.file_reader_utils.ods_reader_headers","title":"tablite.file_reader_utils.ods_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount)","text":"Source code in tablite/file_reader_utils.py
    def ods_reader_headers(path, delimiter, header_row_index, text_qualifier, linecount):\n    d = {\n        \"delimiter\": None\n    }\n    sheets = pyexcel.get_book_dict(file_name=str(path))\n\n    for sheet_name, data in sheets.items():\n        lines = [[DataTypes.to_json(v) for v in row] for row in data[header_row_index:header_row_index+linecount]]\n\n        d[sheet_name] = lines\n\n    return d\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    kwargs = {\n        \"path\": path,\n        \"delimiter\": delimiter,\n        \"header_row_index\": header_row_index,\n        \"text_qualifier\": text_qualifier,\n        \"linecount\": linecount\n   }\n\n    reader = header_readers.get(path.suffix[1:], None)\n\n    if reader is None:\n        raise TypeError(f\"file format for headers not supported: {path.suffix}\")\n\n    result = reader(**kwargs)\n\n    return result\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.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/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, BaseTable):\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, BaseTable):\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, BaseTable):\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, BaseTable):\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, skip_empty='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, skip_empty=\"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, BaseTable):\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        \"\"\"\n            If no sheet specified, assume first sheet.\n\n            Reasoning:\n                Pandas ODS reader does that, so this preserves parity and it might be expected by users.\n                If we don't know the sheet name but only have single sheet,\n                    we would need to take extra steps to find out the name of the sheet.\n                We already make assumptions in case of column selection,\n                    when columns are None, we import all of them.\n        \"\"\"\n        sheet = book.sheetnames[0]\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        it_header = worksheet.iter_rows(min_row=header_row_index + 1)\n        while True:\n            # get the first row to know our headers or the number of columns\n            row = [c.value for c in next(it_header)]\n            break\n        fields = [str(c) if c is not None else \"\" for c in row] # 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 = iter(tqdm_iter)\n\n    idx = 0\n\n    while True:\n        try:\n            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 skip_empty == \"ALL\" and all(v is None for v in row):\n            continue\n        elif skip_empty == \"ANY\" and any(v is None for v in row):\n            continue\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        idx = idx + 1\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, skip_empty='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, skip_empty=\"NONE\", start=0, limit=sys.maxsize, **kwargs):\n    \"\"\"\n    returns Table from .ODS\n    \"\"\"\n    if not issubclass(T, BaseTable):\n        raise TypeError(\"Expected subclass of Table\")\n\n    if sheet is None:\n        data = read_excel(str(path), header=None) # selects first sheet\n    else:\n        data = read_excel(str(path), sheet_name=sheet, header=None)\n\n    data[isna(data)] = None  # convert any empty cells to None\n    data = data.to_numpy().tolist() # convert pandas to list\n\n    if skip_empty == \"ALL\" or skip_empty == \"ANY\":\n        \"\"\" filter out all rows based on predicate that come after header row \"\"\"\n        fn_filter = any if skip_empty == \"ALL\" else all # this is intentional\n        data = [\n            row\n            for ridx, row in enumerate(data)\n            if ridx < header_row_index + (1 if first_row_has_headers else 0) or fn_filter(not (v is None or isinstance(v, str) and len(v) == 0) for v in row)\n        ]\n\n    data = np.array(data, dtype=np.object_) # cast back to numpy array for slicing but don't try to convert datatypes\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 = \"\" if value is None else 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        column_values = data[start_row_pos : start_row_pos + limit, ix]\n\n        t[unique_column_name] = column_values\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, skip_empty, 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    skip_empty,\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    enc = py_to_nim_encoding(encoding)\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    if skip_empty is None:\n        kwargs[\"skip_empty\"] = \"NONE\"\n    else:\n        kwargs[\"skip_empty\"] = skip_empty\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, BaseTable)\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=pbar)\n    elif method in {\"mean\", \"mode\"}:\n        return stats_method(T, targets, missing, method, tqdm=tqdm, pbar=pbar)\n    elif method == \"nearest neighbour\":\n        return nearest_neighbour(T, sources, missing, targets, tqdm=tqdm)\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 = T.copy()\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 = T.copy()\n    for name in T.columns:\n        if name in targets:\n            col = T.columns[name]\n            assert isinstance(col, Column)\n\n            hist_values, hist_counts = col.histogram()\n\n            for m in missing:\n                try:\n                    idx = hist_values.index(m)\n                    hist_counts[idx] = 0\n                except ValueError:\n                    pass\n\n            stats = summary_statistics(hist_values, hist_counts)\n\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-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: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], kind: str = 'inner', merge_keys: bool = False, 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.

    Example: \"inner\"

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

    Tablite:

    >>> inner_join = numbers.inner_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n)\n

    Example: \"left\"

    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

    Example: \"outer\"

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

    Tablite:

    >>> outer_join = numbers.outer_join(\n    letters, \n    left_keys=['colour'], \n    right_keys=['color'], \n    left_columns=['number'], \n    right_columns=['letter']\n    )\n

    Example: \"cross\"

    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 join(\n    T: BaseTable,\n    other: BaseTable,\n    left_keys: List[str],\n    right_keys: List[str],\n    left_columns: Union[List[str], None],\n    right_columns: Union[List[str], None],\n    kind: str = \"inner\",\n    merge_keys: bool = False,\n    tqdm=_tqdm,\n    pbar=None,\n):\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    Example: \"inner\"\n    ```\n    SQL:   SELECT number, letter FROM numbers JOIN letters ON numbers.colour == letters.color\n    ```\n    Tablite: \n    ```\n    >>> inner_join = numbers.inner_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n    )\n    ```\n\n    Example: \"left\" \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    Example: \"outer\"\n    ```\n    SQL:   SELECT number, letter FROM numbers OUTER JOIN letters ON numbers.colour == letters.color\n    ```\n\n    Tablite: \n    ```\n    >>> outer_join = numbers.outer_join(\n        letters, \n        left_keys=['colour'], \n        right_keys=['color'], \n        left_columns=['number'], \n        right_columns=['letter']\n        )\n    ```\n\n    Example: \"cross\"\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}\n\n    _jointype_check(T, other, left_keys, right_keys, left_columns, right_columns)\n\n    return _join(kind, T,other,left_keys, right_keys, left_columns, right_columns, merge_keys=merge_keys,\n             tqdm=tqdm, pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.inner_join","title":"tablite.joins.inner_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def inner_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"inner\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.left_join","title":"tablite.joins.left_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def left_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"left\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.outer_join","title":"tablite.joins.outer_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def outer_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"outer\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\n
    "},{"location":"reference/joins/#tablite.joins.cross_join","title":"tablite.joins.cross_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], left_columns: Union[List[str], None], right_columns: Union[List[str], None], merge_keys: bool = False, tqdm=_tqdm, pbar=None)","text":"Source code in tablite/joins.py
    def cross_join(T: BaseTable, other: BaseTable, left_keys: List[str], right_keys: List[str], \n              left_columns: Union[List[str], None], right_columns: Union[List[str], None],\n              merge_keys: bool = False, tqdm=_tqdm, pbar=None):\n    return join(T, other, left_keys, right_keys, left_columns, right_columns, kind=\"cross\", merge_keys=merge_keys, tqdm=tqdm,pbar=pbar)\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, BaseTable)\n    sub_cls_check(other, BaseTable)\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    Constr = type(T)\n    if isinstance(left, Column):\n        tmp, left = left, Constr()\n        left[left_columns[0]] = tmp\n    right = other[right_columns]\n    if isinstance(right, Column):\n        tmp, right = right, Constr()\n        right[right_columns[0]] = tmp\n    assert isinstance(left, BaseTable)\n    assert isinstance(right, BaseTable)\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, BaseTable)\n    assert isinstance(other, BaseTable)\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 and creates a single new column.

    :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    and creates a single new column.\n\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, BaseTable)\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_uq = unique_name(new, list(T.columns))\n    T.add_column(new_uq)\n    col = T[new_uq]\n\n    for start,end in Config.page_steps(len(criteria)):\n        left_values = T[left][start:end]\n        right_values = T[right][start:end]\n        new_values = np.where(criteria, left_values, right_values)\n        col.extend(new_values)\n\n    if new == right:\n        T[right] = T[new_uq]  # keep column order\n        del T[new_uq]\n        del T[left]\n    elif new == left:\n        T[left] = T[new_uq]  # keep column order\n        del T[new_uq]\n        del T[right]\n    else:\n        T[new] = T[new_uq]\n        del T[left]\n        del T[right]\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.is_mp","title":"tablite.mp_utils.is_mp(fields: int) -> bool","text":"PARAMETER DESCRIPTION fields

    number of fields

    TYPE: int

    RETURNS DESCRIPTION bool

    bool

    Source code in tablite/mp_utils.py
    def is_mp(fields: int) -> bool:\n    \"\"\"\n\n    Args:\n        fields (int): number of fields\n\n    Returns:\n        bool\n    \"\"\"\n    if Config.MULTIPROCESSING_MODE == Config.FORCE:\n        return True\n\n    if Config.MULTIPROCESSING_MODE == Config.FALSE:\n        return False\n\n    if fields < Config.SINGLE_PROCESSING_LIMIT:\n        return False\n\n    if max(psutil.cpu_count(logical=False), 1) < 2:\n        return False\n\n    return True\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    return mp if is_mp(fields) else sp\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.paths","title":"tablite.nimlite.paths = sys.argv[:] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.K","title":"tablite.nimlite.K = TypeVar('K', bound=BaseTable) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidEncoders","title":"tablite.nimlite.ValidEncoders = Literal['ENC_UTF8', 'ENC_UTF16', 'ENC_WIN1250'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidQuoting","title":"tablite.nimlite.ValidQuoting = Literal['QUOTE_MINIMAL', 'QUOTE_ALL', 'QUOTE_NONNUMERIC', 'QUOTE_NONE', 'QUOTE_STRINGS', 'QUOTE_NOTNULL'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ValidSkipEmpty","title":"tablite.nimlite.ValidSkipEmpty = Literal['NONE', 'ANY', 'ALL'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.ColumnSelectorDict","title":"tablite.nimlite.ColumnSelectorDict = TypedDict('ColumnSelectorDict', {'column': str, 'type': Literal['int', 'float', 'bool', 'str', 'date', 'time', 'datetime'], 'allow_empty': Union[bool, None], 'rename': Union[str, None]}) module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterCriteria","title":"tablite.nimlite.FilterCriteria = Literal['>', '>=', '==', '<', '<=', '!=', 'in'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterType","title":"tablite.nimlite.FilterType = Literal['all', 'any'] module-attribute","text":""},{"location":"reference/nimlite/#tablite.nimlite.FilterDict","title":"tablite.nimlite.FilterDict = TypedDict('FilterDict', {'column1': str, 'value1': Union[str, None], 'criteria': FilterCriteria, 'column2': str, 'value2': Union[str, None]}) 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.get_headers","title":"tablite.nimlite.get_headers(path: Union[str, Path], encoding: ValidEncoders = 'ENC_UTF8', *, header_row_index: int = 0, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool = True, linecount: int = 10) -> list[list[str]]","text":"Source code in tablite/nimlite.py
    def get_headers(\n    path: Union[str, Path],\n    encoding: ValidEncoders =\"ENC_UTF8\",\n    *,\n    header_row_index: int=0,\n    newline: str='\\n', delimiter: str=',', text_qualifier: str='\"',\n    quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool=True,\n    linecount: int = 10\n) -> list[list[str]]:\n    return nl.get_headers(\n            path=str(path),\n            encoding=encoding,\n            newline=newline, delimiter=delimiter, text_qualifier=text_qualifier,\n            strip_leading_and_tailing_whitespace=strip_leading_and_tailing_whitespace,\n            header_row_index=header_row_index,\n            quoting=quoting,\n            linecount=linecount\n        )\n
    "},{"location":"reference/nimlite/#tablite.nimlite.text_reader","title":"tablite.nimlite.text_reader(T: Type[K], pid: str, path: Union[str, Path], encoding: ValidEncoders = 'ENC_UTF8', *, first_row_has_headers: bool = True, header_row_index: int = 0, columns: List[Union[str, None]] = None, start: Union[str, None] = None, limit: Union[str, None] = None, guess_datatypes: bool = False, newline: str = '\\n', delimiter: str = ',', text_qualifier: str = '\"', quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool = True, skip_empty: ValidSkipEmpty = 'NONE', tqdm=_tqdm) -> K","text":"Source code in tablite/nimlite.py
    def text_reader(\n    T: Type[K],\n    pid: str, path: Union[str, Path],\n    encoding: ValidEncoders =\"ENC_UTF8\",\n    *,\n    first_row_has_headers: bool=True, header_row_index: int=0,\n    columns: List[Union[str, None]]=None,\n    start: Union[str, None] = None, limit: Union[str, None]=None,\n    guess_datatypes: bool =False,\n    newline: str='\\n', delimiter: str=',', text_qualifier: str='\"',\n    quoting: ValidQuoting, strip_leading_and_tailing_whitespace: bool=True, skip_empty: ValidSkipEmpty = \"NONE\",\n    tqdm=_tqdm\n) -> K:\n    assert isinstance(path, Path)\n    assert isinstance(pid, Path)\n    with tqdm(total=10, desc=f\"importing file\") as pbar:\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            skip_empty=skip_empty,\n            page_size=Config.PAGE_SIZE\n        )\n\n        pbar.update(1)\n\n        task_info = table[\"task\"]\n        task_columns = table[\"columns\"]\n\n        ti_tasks = task_info[\"tasks\"]\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        pbar_step = 4 / max(len(ti_tasks), 1)\n\n        class WrapUpdate:\n            def update(self, n):\n                pbar.update(n * pbar_step)\n\n        wrapped_pbar = WrapUpdate()\n\n        def next_task(task: Task, page_info):\n            wrapped_pbar.update(1)\n            return Task(\n                nl.text_reader_task,\n                *task.args, **task.kwargs, page_info=page_info\n            )\n\n        tasks = [\n            TaskChain(\n                Task(\n                    nl.collect_text_reader_page_info_task,\n                    task=t,\n                    task_info=task_info\n                ), next_task=next_task\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.FORCE:\n            is_sp = False\n        elif Config.MULTIPROCESSING_MODE == Config.AUTO and cpus <= 1 or len(tasks) <= 1:\n            is_sp = True\n\n        if is_sp:\n            res = []\n\n            for task in tasks:\n                page = task.execute()\n\n                res.append(page)\n        else:\n            with TaskManager(cpus, error_mode=\"exception\") as tm:\n                res = tm.execute(tasks, pbar=wrapped_pbar)\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        pbar.update(pbar.total - pbar.n)\n\n        table = T(columns=table_dict)\n\n    return table\n
    "},{"location":"reference/nimlite/#tablite.nimlite.wrap","title":"tablite.nimlite.wrap(str_: str) -> str","text":"Source code in tablite/nimlite.py
    def wrap(str_: str) -> str:\n    return '\"' + str_.replace('\"', '\\\\\"').replace(\"'\", \"\\\\'\").replace(\"\\n\", \"\\\\n\").replace(\"\\t\", \"\\\\t\") + '\"'\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: K, cols: list[ColumnSelectorDict], tqdm=_tqdm, TaskManager=TaskManager) -> Tuple[K, K]:\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        col_infos = nl.collect_column_select_info(table, cols, str(dir_pid), pbar)\n\n        columns = col_infos[\"columns\"]\n        page_count = col_infos[\"page_count\"]\n        is_correct_type = col_infos[\"is_correct_type\"]\n        desired_column_map = col_infos[\"desired_column_map\"]\n        original_pages_map = col_infos[\"original_pages_map\"]\n        passed_column_data = col_infos[\"passed_column_data\"]\n        failed_column_data = col_infos[\"failed_column_data\"]\n        res_cols_pass = col_infos[\"res_cols_pass\"]\n        res_cols_fail = col_infos[\"res_cols_fail\"]\n        column_names = col_infos[\"column_names\"]\n        reject_reason_name = col_infos[\"reject_reason_name\"]\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, original_pages_map)\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)\n\n        if is_mp:\n            class WrapUpdate:\n                def update(self, n):\n                    pbar.update(n * step_size)\n\n            with TaskManager(min(cpu_count, page_count), error_mode=\"exception\") as tm:\n                res = tm.execute(list(tasks), pbar=WrapUpdate())\n\n                converted.extend(res)\n        else:\n            for task in tasks:\n                res = task.f(*task.args, **task.kwargs)\n\n                converted.append(res)\n                pbar.update(step_size)\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: Union[str, Path]) -> np.ndarray","text":"Source code in tablite/nimlite.py
    def read_page(path: Union[str, Path]) -> np.ndarray:\n    return nl.read_page(str(path))\n
    "},{"location":"reference/nimlite/#tablite.nimlite.repaginate","title":"tablite.nimlite.repaginate(column: Column)","text":"Source code in tablite/nimlite.py
    def repaginate(column: Column):\n    nl.repaginate(column)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.nearest_neighbour","title":"tablite.nimlite.nearest_neighbour(T: BaseTable, sources: Union[list[str], None], missing: Union[list, None], targets: Union[list[str], None], tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def nearest_neighbour(T: BaseTable, sources: Union[list[str], None], missing: Union[list, None], targets: Union[list[str], None], tqdm=_tqdm):\n    return nl.nearest_neighbour(T, sources, list(missing), targets, tqdm)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.groupby","title":"tablite.nimlite.groupby(T, keys, functions, tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def groupby(T, keys, functions, tqdm=_tqdm):\n    return nl.groupby(T, keys, functions, tqdm)\n
    "},{"location":"reference/nimlite/#tablite.nimlite.filter","title":"tablite.nimlite.filter(table: BaseTable, expressions: list[FilterDict], type: FilterType, tqdm=_tqdm)","text":"Source code in tablite/nimlite.py
    def filter(table: BaseTable, expressions: list[FilterDict], type: FilterType, tqdm = _tqdm):\n    return nl.filter(table, expressions, type, tqdm)\n
    "},{"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, BaseTable)\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)\n    Constr = type(T)\n\n    if len(grpby) == 0:  # return empty table. This must be a test?\n        pbar.update(extra_steps)\n        return Constr()\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}({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}({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, BaseTable)\n\n    if len(T.columns) == 0:\n        return type(T)()\n\n    assert isinstance(T, BaseTable)\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, BaseTable)\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        it = T[keep + columns].rows if len(keep + columns) > 1 else ((v, ) for v in T[keep + columns])\n\n        for ix, row in enumerate(it, 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, BaseTable)\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, page in col.iter_by_page():\n            data = page.get()\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, BaseTable)\n    mask = np.full((len(T),), False)\n    for name in T.columns:\n        col = T[name]\n        for start, end, page in col.iter_by_page():\n            data = page.get()\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, BaseTable)\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, page in col.iter_by_page():\n            data = page.get()\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_non_primitive","title":"tablite.redux.filter_non_primitive(T, expressions, filter_type='all', tqdm=_tqdm)","text":"

    OBSOLETE 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_non_primitive(T, expressions, filter_type=\"all\", tqdm=_tqdm):\n    \"\"\"\n    OBSOLETE\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    warnings.warn(\"Filter using non-primitive types is not recommended.\")\n    sub_cls_check(T, BaseTable)\n    if len(T) == 0:\n        return T.copy(), T.copy()\n\n    with tqdm(desc=\"filter\", total=20) as pbar:\n        if isinstance(expressions, str):\n            mask = _filter_using_expression(T, expressions)\n            pbar.update(10)\n        elif isinstance(expressions, list):\n            mask = _filter_using_list_of_dicts(T, expressions, filter_type, pbar)\n        else:\n            raise TypeError\n        # create new tables\n        res = _compress_both(T, mask, pbar=pbar)\n        pbar.update(pbar.total - pbar.n)\n\n        return res\n
    "},{"location":"reference/redux/#tablite.redux.filter","title":"tablite.redux.filter(T, expressions, filter_type='all', tqdm=_tqdm)","text":"

    filters table Note: At the moment only tablite primitive types are supported

    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    Note: At the moment only tablite primitive types are supported\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, BaseTable)\n    if len(T) == 0:\n        return T.copy(), T.copy()\n\n    if isinstance(expressions, str):\n        with tqdm(desc=\"filter\", total=20) as pbar:\n            # TODO: make parser for expressions and use the nim implement\n            mask = _filter_using_expression(T, expressions)\n            pbar.update(10)\n            res = _compress_both(T, mask, pbar=pbar)\n            pbar.update(pbar.total - pbar.n)\n    elif isinstance(expressions, list):\n        return _filter_using_list_of_dicts_native(T, expressions, filter_type, tqdm)\n    else:\n        raise TypeError\n        # create new tables\n\n    return res\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, BaseTable)\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-classes","title":"Classes","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict","title":"tablite.sort_utils.HashDict","text":"

    Bases: dict

    This class is just a nicity syntatic sugar for debugging. Function identically to regular dictionary, just uses tupled key.

    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict-functions","title":"Functions","text":""},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.items","title":"tablite.sort_utils.HashDict.items()","text":"Source code in tablite/sort_utils.py
    def items(self):\n    return [(k, v) for (_, k), v in super().items()]\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.keys","title":"tablite.sort_utils.HashDict.keys()","text":"Source code in tablite/sort_utils.py
    def keys(self):\n    return [k for (_, k) in super().keys()]\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__iter__","title":"tablite.sort_utils.HashDict.__iter__() -> Iterator","text":"Source code in tablite/sort_utils.py
    def __iter__(self) -> Iterator:\n    return (k for (_, k) in super().keys())\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__getitem__","title":"tablite.sort_utils.HashDict.__getitem__(key)","text":"Source code in tablite/sort_utils.py
    def __getitem__(self, key):\n    return super().__getitem__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__setitem__","title":"tablite.sort_utils.HashDict.__setitem__(key, value)","text":"Source code in tablite/sort_utils.py
    def __setitem__(self, key, value):\n    return super().__setitem__(self._get_hash(key), value)\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__contains__","title":"tablite.sort_utils.HashDict.__contains__(key) -> bool","text":"Source code in tablite/sort_utils.py
    def __contains__(self, key) -> bool:\n    return super().__contains__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__delitem__","title":"tablite.sort_utils.HashDict.__delitem__(key)","text":"Source code in tablite/sort_utils.py
    def __delitem__(self, key):\n    return super().__delitem__(self._get_hash(key))\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__repr__","title":"tablite.sort_utils.HashDict.__repr__() -> str","text":"Source code in tablite/sort_utils.py
    def __repr__(self) -> str:\n    return '{' + \", \".join([f\"{k}: {v}\" for k, v in self.items()]) + '}'\n
    "},{"location":"reference/sort_utils/#tablite.sort_utils.HashDict.__str__","title":"tablite.sort_utils.HashDict.__str__() -> str","text":"Source code in tablite/sort_utils.py
    def __str__(self) -> str:\n    return repr(self)\n
    "},{"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    d = HashDict()\n    L = non_text + text\n    for ix, (_, _, value) in enumerate(L):\n        d[value] = ix\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, BaseTable)\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, BaseTable)\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, BaseTable)\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.NoneType","title":"tablite.utils.NoneType = type(None) 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-classes","title":"Classes","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    max_col, max_row = 0, 0\n\n    regex = re.compile(\"\\d+\")\n\n    def handleStartElement(name, attrs):\n        nonlocal max_col, max_row\n\n        if name == \"c\":\n            last_index = attrs[\"r\"]\n            idx, _ = next(regex.finditer(last_index)).span()\n            letters, digits = last_index[0:idx], int(last_index[idx:])\n\n            col_idx, row_idx = calc_col_count(letters), digits\n\n            max_col, max_row = max(max_col, col_idx), max(max_row, row_idx)\n\n    parser = expat.ParserCreate()\n    parser.buffer_text = True\n    parser.StartElementHandler = handleStartElement\n    parser.ParseFile(src)\n\n    return max_col, max_row\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/utils/#tablite.utils.select_type_name","title":"tablite.utils.select_type_name(dtypes: dict)","text":"Source code in tablite/utils.py
    def select_type_name(dtypes: dict):\n    dtypes = [t for t in dtypes.items() if t[0] != NoneType]\n\n    if len(dtypes) == 0:\n        return \"empty\"\n\n    (best_type, _), *_ = sorted(dtypes, key=lambda t: t[1], reverse=True)\n\n    return best_type.__name__\n
    "},{"location":"reference/utils/#tablite.utils.get_predominant_types","title":"tablite.utils.get_predominant_types(table, all_dtypes=None)","text":"Source code in tablite/utils.py
    def get_predominant_types(table, all_dtypes=None):\n    if all_dtypes is None:\n        all_dtypes = table.types()\n\n    dtypes = {\n        k: select_type_name(v)\n        for k, v in all_dtypes.items()\n    }\n\n    return dtypes\n
    "},{"location":"reference/utils/#tablite.utils.py_to_nim_encoding","title":"tablite.utils.py_to_nim_encoding(encoding: str) -> str","text":"Source code in tablite/utils.py
    def py_to_nim_encoding(encoding: str) -> str:\n    if encoding is None or encoding.lower() in [\"ascii\", \"utf8\", \"utf-8\", \"utf-8-sig\"]:\n        return \"ENC_UTF8\"\n    elif encoding.lower() in [\"utf16\", \"utf-16\"]:\n        return \"ENC_UTF16\"\n    elif encoding in Config.NIM_SUPPORTED_CONV_TYPES:\n        return f\"ENC_CONV|{encoding}\"\n\n    raise NotImplementedError(f\"encoding not implemented: {encoding}\")\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 b/master/sitemap.xml index dc824927..33a06231 100644 --- a/master/sitemap.xml +++ b/master/sitemap.xml @@ -2,147 +2,147 @@ https://root-11.github.io/tablite/master/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/benchmarks/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/changelog/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/tutorial/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/base/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/config/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/core/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/datasets/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/datatypes/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/diff/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/export_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/file_reader_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/groupby_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/import_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/imputation/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/joins/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/lookup/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/match/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/merge/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/mp_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/nimlite/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/pivots/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/redux/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/reindex/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/sort_utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/sortation/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/tools/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/utils/ - 2024-04-05 + 2024-04-08 daily https://root-11.github.io/tablite/master/reference/version/ - 2024-04-05 + 2024-04-08 daily \ No newline at end of file diff --git a/master/sitemap.xml.gz b/master/sitemap.xml.gz index 31950fe1e8604cccecc88fcbfce50c4f366765c1..7e87b7a6ee92898da102cd49c070bfdd61f86b5f 100644 GIT binary patch delta 375 zcmV--0f_#K1BwF&ABzYGD#R0!2OfWR=q5^UdxGs;1u!9Nh%?9OpSNFVtEu;C2SZ%Uod z=PFu;NrzY=85O%RKScEis#Wz`Hj5&US>S$m8@0o(nN?zzAw*uT*Ojq6rnY~ws93iy zOQ@{HK&n=A`t#d4K4IT%R-2dQD$i|BdBmR5x>0)r1HpIGYwNDHdWzh!)Gybc2;1!0 zIB7aY0kC&nheiM^Cq}Z`2|y(nfN~vxYIOurz#aO*8ezyJ%i~^iE(?z0p!pg=7_}V+ zQNVBsNPI|aoh+vZ>)=43dmv|l^oB5n_9~FvPC^SNgQvWuvqOhFX# zq`4bq5d#@nUC0y!ZW`WBw*^Ve!Jbf9vq#XUi!fsAdyvH(Wggr!w!Cwj5rz3IuQDD# V5C5O}cFCX6%?~OHP?EJ2005wct&so# delta 376 zcmV-;0f+vI1B(L(ABzYG_{tBF2OfVmX`3j$?FqJX6~I8&B+eYCf8Ktft)||m9UMgh z9P{a?PgY{6cV}mdBTzKxx8=H8l?C_?d*jD#`T6m_d?|O$qE2KZaFLUJ+?Eq_cvI?h zI#tmyI2~ezG^*H5`5~%%P_3%hvRM>)%mTN&>!>Yu&8!l$3?cGzy{<;XQ)+)JgNk)) zvxLf845Vr`r$4@(;|}{~v)Vi@S9w0?lzZ$Ut(&wTVIcT!dTs2bRu7RImip!T6JeV@ zo1CUY6aahEwKxi3<-|xvTLGv915mC5P_2$23b;c*m{Axq$?~|@oXdivIB32E5Jqi= zK@>2Y0}}5O8!OA{!C2T6=pJZDApJ;~LVFQNZk*79$>1q(DY@Ach5d!b%QD&`9#Rm6 z>@;_iEMlNhRu|%ez)iy&cU_Rg9Ly1gHG2eoItwE": + result = dset_A > dset_B + elif expr == ">=": + result = dset_A >= dset_B + elif expr == "==": + result = dset_A == dset_B + elif expr == "<": + result = dset_A < dset_B + elif expr == "<=": + result = dset_A <= dset_B + elif expr == "!=": + result = dset_A != dset_B + else: # it's a python evaluations (slow) + f = filter_ops.get(expr) + assert callable(f) + result = list_to_np_array([f(a, b) for a, b in zip(dset_A, dset_B)]) + except TypeError: + def safe_test(f, a, b): + try: + return f(a, b) + except TypeError: + return False + f = filter_ops.get(expr) + assert callable(f) + result = list_to_np_array([safe_test(f, a, b) for a, b in zip(dset_A, dset_B)]) + bitmap[bit_index, start:end] = result + pbar.update(pbar_step) + + f = np.all if filter_type == "all" else np.any + mask = f(bitmap, axis=0) + # 4. The mask is now created and is no longer needed. + pbar.update(10 - pbar.n) + return mask + +def filter_non_primitive(T, expressions, filter_type="all", tqdm=_tqdm): + """ + OBSOLETE + filters table + + + Args: + T (Table subclass): Table. + expressions (list or str): + str: + filters based on an expression, such as: + "all((A==B, C!=4, 200