From 062ad14343b08e64234b2b207416ead49107b8f2 Mon Sep 17 00:00:00 2001 From: Benjamin Bengfort Date: Fri, 18 Feb 2022 23:20:32 -0600 Subject: [PATCH] PrePredict Estimator (#1189) --- docs/api/contrib/index.rst | 1 + docs/api/contrib/prepredict.rst | 52 +++++ .../test_prepredict_classifier.png | Bin 0 -> 23830 bytes .../test_prepredict_regressor.png | Bin 0 -> 19686 bytes tests/test_contrib/test_prepredict.py | 183 ++++++++++++++++++ yellowbrick/contrib/prepredict.py | 104 ++++++++++ 6 files changed, 340 insertions(+) create mode 100644 docs/api/contrib/prepredict.rst create mode 100644 tests/baseline_images/test_contrib/test_prepredict/test_prepredict_classifier.png create mode 100644 tests/baseline_images/test_contrib/test_prepredict/test_prepredict_regressor.png create mode 100644 tests/test_contrib/test_prepredict.py create mode 100644 yellowbrick/contrib/prepredict.py diff --git a/docs/api/contrib/index.rst b/docs/api/contrib/index.rst index fec517dcb..30fe1510e 100644 --- a/docs/api/contrib/index.rst +++ b/docs/api/contrib/index.rst @@ -13,6 +13,7 @@ The following contrib packages are currently available: :maxdepth: 1 wrapper + prepredict statsmodels boundaries scatter diff --git a/docs/api/contrib/prepredict.rst b/docs/api/contrib/prepredict.rst new file mode 100644 index 000000000..beea3c7f5 --- /dev/null +++ b/docs/api/contrib/prepredict.rst @@ -0,0 +1,52 @@ +.. -*- mode: rst -*- + +PrePredict Estimators +===================== + +Occassionally it is useful to be able to use predictions made during an inferencing workflow that does not involve Yellowbrick, for example when the inferencing process requires extra compute resources such as a cluster or when the model takes a very long time to train and inference. In other instances there are models that Yellowbrick simply does not support, even with the :doc:`third-party estimator wrapper ` or the results may have been collected from some source out of your control. + +Some Yellowbrick visualizers are still able to create visual diagnostics with predictions already made using the contrib library ``PrePredict`` estimator, which is a simple wrapper around some data and an estimator type. Although not quite as straight forward as a scikit-learn metric in the form ``metric(y_true, y_pred)``, this estimator allows Yellowbrick to be used in the cases described above, an example is below: + +.. code:: python + + # Import the prepredict estimator and a Yellowbrick visualizer + from yellowbrick.contrib.prepredict import PrePredict, CLASSIFIER + from yellowbrick.classifier import classification_report + + # Instantiate the estimator with the pre-predicted data + model = PrePredict(y_pred, CLASSIFIER) + + # Use the visualizer, setting X to None since it is not required + oz = classification_report(model, None, y_test) + oz.show() + +.. warning:: Many Yellowbrick visualizers inspect the estimator for learned attributes in order to deliver rich diagnostics. You may run into visualizers that cannot use the prepredict method, or you can manually set attributes on the ``PrePredict`` estimator with the learned attributes the visualizer requires. + +In the case where you've saved pre-predicted data from disk, the ``PrePredict`` estimator can load it using ``np.load``. A full workflow is described below: + +.. code:: python + + # Phase one: fit your estimator, make inferences, and save the inferences to disk + np.save("y_pred.npy", y_pred) + + # Import the prepredict estimator and a Yellowbrick visualizer + from yellowbrick.contrib.prepredict import PrePredict, REGRESSOR + from yellowbrick.regressor import prediction_error + + # Instantiate the estimator with the pre-predicted data and pass a path to where + # the data has been saved on disk. + model = PrePredict("y_pred.npy", REGRESSOR) + + # Use the visualizer, setting X to None since it is not required + oz = prediction_error(model, X_test, y_test) + oz.show() + +The ``PrePredict`` estimator can use a callable function to return pre-predicted data, a ``str``, file-like object, or ``pathlib.Path`` to load from disk using ``np.load``, otherwise it simply returns the data it wraps. See the API reference for more details. + +API Reference +------------- + +.. automodule:: yellowbrick.contrib.prepredict + :members: PrePredict + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/tests/baseline_images/test_contrib/test_prepredict/test_prepredict_classifier.png b/tests/baseline_images/test_contrib/test_prepredict/test_prepredict_classifier.png new file mode 100644 index 0000000000000000000000000000000000000000..bd810c8b3c4a4ef76eb99e627ca96169e4931da7 GIT binary patch literal 23830 zcmdqJcUY6{+UAP|Q4n}7fP#QZm8#Oaf(Q}mgbopq-lPPiV?h)Ylq$W~5JC?G0xAO1 zYX~i%(g_490ZB;MPkh&!Su?Z0W4@U^$FbMmf9OLX$#Xw$Yf8yco@8N2HHNer= z&(+&YNdPoMw5a_!ogYiMlR0p;AKD^huBGcSKpl;c_Z()((%)@*X@7ZppPrtQ(J*qEo$A^q}jH@9-HHc9{ykJX2S9~60^ zRs|_@N0glK!>z7U1lOI#sR?dcg}wEi4O(7p*%?q;CFO)}zp+Gm!L@jT?j?*>J7{@M z)q(t$`i9?M)8H)6=(kK{x|$*9ozm}=n2}-S5J(TS)msxVSRk>xFk2-=Ho$B>fG|_m z)6;XxbsC4W0ZPL>vX>%mCH?3VU3AUo7YumdvAo|8Dsf3S(b?BdKCYeQY~}(=;apMt z+orsx#%Qs~QA8=p7_ku?#ej7WiAc%Hg1>+NS9A^V9~%b;tfj>|J3E`1bkJ`I6h_Ng z6%`R;#$8dEFRi!CpPD?|=RDRWLT5}L+1iC?CHx|1vnFmET zym{53xdPnv4!63xdQw)_h5>LJaR>zBq^5V_Pj@QSP*;B*PFfX;u7TpmZ(O^U=c71w z_D?^2GA?}M#_-<8bbS~(05k$T6zzwik`kSSV*Q-T?nI$!JR}eYeW0t0(UC+KqC@uG zK0l`?k?;uUPQRSo{r!w6!9ZqO&mJ-eCvV8%p3cA9!;Ti~Ci2^a5i!9(qBxS0lZ)QI zJxeA74KS><*;3#-TI@R862!>Lime?~?G=A|bo|fQ7~rIkh@%5pO8C&DC=S~vsD2Je z=x`)jvAOCb6L5!r%VE)3U#%2LGBotWB=)kpP>aPDlTAfcN38TABc|;R$K#uEW8N$) zt4BrSBKuEd%62>M?qMCl?6}&eIkJTpZ^*Q%v%_?`U#Q5mdVeHbxp+-(r&^5MK6_jj zl3XnA+R-CYW)ztU3%*BMIeMk#Y7lZnns)upFTFb%2rc`bc-eSJ9i0d^{xC#_|u<8NMbAq753=mm&2JPh$Wo9?eXfNT!Y&x90vuZ&hySh zlE#J{G!kY(eM2@(`)ldHAK@ZwMd6cXx#ZS~k`WP@AgjA4!pz!knBH5kEO#bfOVWi; zt`>x#ysymj_P|K85>2Y&ttji#PtL!+#3hMPF3o3*CEYu?QBbwrxY0lRzHC}`8qTD| zu+pS6-P6%gXk+21_z~i>ro@m7;)|3%!DSS)yPD#G_7750k*-_n9e9Vf5}__acE3d= z)Gy8Q2zfa8hrU)1#Z$L0ENm1*_Z1rax;~w*|K*S2a+i{(#OyKd2d47lrqryFd#r(_ zA;Hf^%5LAZKn2oZxSD^u)q*q;{@&rGff`akugS zOdg*)yD=bv)!!!?cN_VB8XW-cGM>In=X|B|nCmHaO1gM$tp9YR1B9f4v-a0%yD7jU zY~(ikW{_u+pDo3#*QqvZ#2Lx7{hog@70ipwMmAgIsd|YB*Jc`#KSAuiPW%qnf9CrD z1_d4{db|0QW@+I_`H|P4%6JZZa&5E|%cII)rt=Z`3d%UNY*nN0vl%(+Jmyp2`M%_# zp3{BmdKNVLarKlm@n=JVn-h~h@#)F$eV@;2OI0br?+xeKZtwda&P|r+_NQiwuXeay zSn-*WRa9v+>|kR*Xl!2TnRtZu)eMO`Xu_78S$Kl&nlv53rk={?g{!cUG1(!FEcF&Q zjJO!W)UjT7QUFYPGHOQ&C^d<<9YUF^4y1RVeM{V_veJw!kR@Pnv0n2odxV|&g>6e?-I9ggixsKYLVmgj`X4$IoZ^p z?2e=vPDWT=-{y({N9u*>OLWdGasK_E^70>O${!T*`aA}ikbK5*r-GZ-2zSq|%?;@D zR7zOZA`HAM`-cW^XiojQqYi&D`m^u~?q);@1-dEQnhy1JZc2COy;7Nl)Hs?>lLYz7 zWk~znX{35T9a~+${=^dK(wC!>Z#Qd;AMGj-DkT{Ae2jzd_B$`6n8Na>oQ~E}qzf8! z&yKxS_R4&Vn{>RL@i6_@sszQs#l1s|Ad}nihFjOxE-b>@BqxfH<4!I;^qyB{d8f*vTANf%4;(R; zO-c}}Q5_Sb)ldseUGLQD=biGTbVwen91qY2z+H;R?5^;2-nU2w6P%V|7AiJ2CIJgl zan#ixg_Y5;?mU*Ep+5!v$rxMFkUrlz%q90WhSku^qTY%6u@V^B{p(kU-acX0mioT9 zl;5*9#SmM-@V6nsbk+)#98^W z#17;?$R6YA5m%u`Ehb2pYrT#U6(jjt4fx=jno%x~_pgeBdJHYtIP%*s-GBS;bVQ&< z#mEn}Fg>7}6a>IA9)>%Y@6J|kKao*Dv#ml_S)V9!?lk`B8y+O@$ncAYx!W#heZMb6 zHz0QMz0=jax#iVr5qv!+o^exmwSorP>ZmfQ_elJs&_Tgg|)Xn6YaUi2{N* zA3E^JWv)UO1X*0^jxl3NwdCGhYE~cli_J1n<{TERDkT=omj%*my{sRSLO8((` z;B8R1C^oLoDB80&2If}3zlYN35vlZjYO59ClRRp5z4gern13}>wu{9yLiK_eW~MlB zYy3+6nO~*QYKb8Opn&a)NQ3uC*sN{p4|BO&8nXlmRK;{Ps^F)_uAf`AY%%*}Zvc-3 zW^~}(F;6o+$9_syx18U|G6>6q1UB3v`imTpc(GF)LrFcuG)1QTVQfNJc z$131XbS!M_)>j=~uRarBuc%qB=}FtorT9Qs;;gzwF>6C=eZ-#gJKfWU)-U6gZEWhC zBJxym2}Lipd36ulDL*(4ea$y3lnO;1USL!wj62vcBq!%q)Ab(gBJR0#g#zA~JxrR@zykc!WumxHZ>&Y1{j_cIR7WNx9nW)RQ;QJ_6b=jwta}P3DL=hF1Tjx|sZt8Mb)72e5$mUkF(({S2 zbx+;ndSGxju9duq%sI<=n+eNVf>Q|k?gph^qDec)qua>Iu*llxVGJ-quA0?h3qOBg z@r>KNP^^!9jIZY%NrE^T<|1Jv%>!3zqbcXekKdGIYgQWZ%pjDX!At9YK{QH47kHX- zU{m8{J!!ko;&X*7JiF2|$SjlR&^IUB>VpVOR}oRr2K@DIp3u{LX@fbSmBa7P?Kl2KJ#xHAdHVNGs{W3w9IW2pr3o^2{w&vSk_*huTzqp}P zx`{xw*-mqlI@}r8hftsm(YSa8U0|R3pHy?duF$04r7t>0VRO>cun9{|HZ$DG`grV*)cq)^bi&?Bv8KA2PWp232OR-W zifFSOtztV47iB>S(9ZYw31^1Bo{gxj4xt-+bNPQ$14q{OJf~-p_L#WY(b3`B_wm*g z3X1o^52i_J0m`BJ=lndBrUwC;W~QkbbrzOi6gW2$Ehc36V zQp2^evczJ&ED?QueR0dWZ?2U73^|w?!j@CPw~!`4X-aXsS5oaq#hJDHf2-R(Kpajt zc#eL0c4EU(LPCO5{z6YbE+lNZ?7mA1qB76s3g2&?WBo~4IE$DtQR=r6MfFcE9LK2 zS*r@C1fv0IOw-i$Pq*wTl8_fE_UJiv(`)z2dJ+mxOC4t?|!yGBdA4 zy?iN;TOwj1ST-~3yju>P+6ngd_9m8=nyLzqgak5CF+$z<(%CzrA$=@INAJJAZG@%Q*I2?C9(i zQB}3|TOI&gSfo`pEpdWI7P=GJ*x8Gtd4jbQr2SV6`mos~AtZsQdedjjT{FJESn;-QuHpPjjQl$;xq;OX-tgaf?e1n~X1eLpKxfdso$u~U zU-@?r|L@Hl_d7tN*{rcP{y=^Ub zI|g=fZU8bk^H;$S6%9Z!CVV}Mu2t3qF)$_@n->kS6V`Yy`T8kzO2 zoHdW-G@o0+p&rg;{NYa#-RxQ$$f3V>5gR-49m|j?(%%>nCr%{|952cJVvk8ws;#?P ze5L+m!ZjZOyEfQf0skGIFy>b}CtLw{ZHf zmt@l8al5QJCz>agWjHD?F6Wk<0OUeLp%NwY1&5~SOd_QFrk-X!SYJX7ST<^s%q-!* zwY^osKvvB0XOxQqB!bCuVMrxNAal%6Tji=bp3;~9K&44p^N6BirceY1_HEdzfn({= zjcSL^e`@8a@peq%2nRzJY2cX4i2Rhyg?R`W%!GAZX%(G<`Mr!^s#GrR&7RFo9%LT+ z?sXUHmFl0X{jAjs75F$L>UR^~AUw(mp$%?}9(Td@zQ7cO>7HJln$q9l(Jrm09D-d* zb4y}2yV1E0^;7-_qg=c0zIi;Yr~>sv9C5tnF>{!hAs2+ZV&H?&eg|tQE7Ajm&EHkW zOX1z`s|D1oPIxS=zWBDGCxlBru7FZ zp+~r4>{XI|332Wc#;}#giR@n6?avr;^83TPbgrjl*Ly~V)Qa&%TQzIQaNz!lw$pH7V zXvHY)1nO*YRB&jUS^QyknT*C1wrNAZsVF$WtihJRQ%qJmg|*l`eB`~+R#mI697j<^ zqdjv6EYSM;8}jSYk=EyzV6(?;OWGVhLF%p*Y3F!#kL#o&M>Vvn`!?WM$SM~Y zj#@atR*$PE^&Ak9ZC=BONkHo%nNK5AgHTN?DdBbxlnn~UT4F9VY;Er|0HzIV)?Cjj zS?RV;eW+G859gHSXF#$eYRY?j52uT!{^R!U5n7*t~SmR!E2 zKcxbw>CUX^>dryyuUdzJj3-WmrbIYQz=m#>(%_hlUOTfu->$kr` z#EpA6E}5+i-^$W!RnrsacV%Vqj#xU8hq)s zHxGi*vC`}nRg!=Xat~o~JYcgaPHGiUlw(3YW)YAo9dCI5P!iKr<{_}|Y&V`Ny!^{o z4)&${bN=Bsjxb5q<F>0F&$2?E9_FwUi9(U|P|xm;RB(OWBvsAicgSQ`t%vcxGL zP_H!YwfAQ6DqU*tUC(EPCFW?t`l+p>8*!nAzEYT%aRt^71>e2d#eUv)D@qz~4gYFi zyGqh!Acpuvsczuzf5;Zf9=>#C9p3uPmL$=8VDl{U70vZ{AcU3SX6DI;&LqR9>_;g& zd#*DI;uxWx&6hdXKn}q#I$hrhlQ;dp)b7Rwermur$ZAj%#UEZvC#UVT?@yb`czVrwz@yb=p`KF}8cjA3}mD z$MnpA35!L+!YSEjK^AomDuzGpE7v>PQTQkjYl%GO8N@%dtk9J9Wy9+B?p&XJ*!#a+ zBlNy?yUZ^ZTYC-AodC)pT2Q)o8}^n@2XUQ}1qr<6?NA)xl5<(M(TBE`bRm{fK#D7?}i9%+1 zoHUV&2a-;oAj1VwC8mo>rByvYf$w?Ml3u@hMm#gxjw2DIV)@j~Av{e4F57hUV_BY} zoqL8d+V^w8Gc7$a9dWtZ4pm7kQ+eVS0V-0*9YD{{^Ve+;m!l6v#1H|Et@auZ4ExDY zWdzOT2UIy_k|$#KM`t_f$-JF7stHyZ!(@sWm+>s*kUy2x;|8nIDZd!}e0pQbLsnoJ zEPO4&;ndK~%@ZSM-|}3#7K4T>?z{@Zn>7N{@;jC%bmt?Q-Oa-fugm zhv9aIB`sL6Nqq+gRbu1m&34?$U~BSA>@=R`2CVTxs=q?LQ0)m9lKkTM+1=PPV;}FH z3-*|0;T}L3l$_@6w+b@oj_#GlXFG75ncZD;f>XuK#t{<8)Q-7YOOD;shVgq;#$D(r zVfTDk)panK9afARD;jrRWCyR|AuR4=k*5!W6^^zptK3vabe&vDNo!l%AN$Ftuda3# zXp$1DDfx5s9gS6v75MN6YH%poj=Qp|G%>z;sc1>Gsm^Fqu&y549ORcC{_O^s$c9^Y zyf96__97`1S1Y5@93#rMxLAC^umjDlcmUZ|3Anq?aXBh@VZVKCeZI1?m0FNOBE$&^q^g&2_=vd19jsIti$kK`_9cAC;YE2wE#|bT~HC zj?t8i^C;Qk-5fsvVkeD$mLdXe>QWI8vF}-^s3lhDv>Lg#Q}Cs*#22k>nt9!30BpmI z)C=PUS)PQ~F#W?q3cGsOkSV~8S}wiFW_F`BD<_3o8$a+)g<7jjXsm;$cq&&cwmmK@ zt*{igYQ*QiG*D42P8_vb3~!ay0Apk+8_LVQ2>xyl+0{1vJ&c-@H?}Q>U}jMS4pT%9 zt&@X=i)EaH>?#yg_=qo4GuW?)8Q3Ok`cQdDHISqMW>snpi)3wVEBAycZKJ7h<3Oa^ z90LsT)-{gBn^s%ycl$~EIDur8t6wlY!1Sz#JL^@4>l7l?sk&9gy220;EwmIB-9{_uUzw1WKfvDTlz8~^aXdM4}U%@ugLeU zk?zVZ*+|8@kk&u$(>Iz%|Mv$m21JT4r40Mk6cU zy~A{HKq>&&h-++msSBtx;KArFIRY+Lx~yk9;8B(Hs06zZW}0YbSBfO0^85GimPAoe zQJL_=r}PXAc);f|GdKS@GCI1rxjAkXmUL3mOFr|hC{2=2JJxn16(rfw0E0$Y=^xGGJ_isDQ|$)F5A3HXUB}& z#MUSI=kx(SjmuuT#d*aCX_fFXFEFJ4cR7+X*JjR=s%0;g<-DsSzVBb8Nu;Q0rL)q zOg#a$^Pn5JCHq9Sr?)q;!@jJ3OhjJZoV>T;htdUT6dFzO4lvy|TgjDx7bOi{GXa~K zxsMcr?)x`SUW=6ef9h9Is$YQaf|3U!#0(i~d;O<7JTW)ESXo3RTh@x3v4Ppi&mNCv zhcc(P=wtqy<#PdFwkv%~}Lv517$Y!rumM68y;m zo=|E5UV6Xz#TD&Gm;Nr1Gwjo+J8At+iwg_#Ny`fh!uy>dLQi3qmyu zf9mSbQ@;Mq*p}c_GEK+)tooRI*TkpEp8v?cMA)K}5)!V%Fm94MdU~cVF2X(B|j=E&(sV_eAti33hj6Y$S`7uxHe&8Wq>1-zd zKkBY_YrYE)urs3x0y=epo3<*%F=G)?(NP(!H6JKlIj9COu)PpR$To`Z!NI{F3p?T{ z9Bx_vV6$Z>&a3gqtInw?DIT4w^k;8d7&T7@kBO-Lhf+6uy>EiBo4(JJfz;sU3zgn~ zc9Jd0WEJp(@XwFPpf(CwS@UGjg9NW;;Rn+IEkTfD*rgsB34ok95~&gbTU24`*G_} z71D8DC}_I;`t+{BeOpcVc7fvut>Q*~(_BRJs}2Ty0mgN&roi(h{aEWnxg%2`unU`n zwAG7QwEuj;%_5&5UE}?6#7{ISGsulY?s!S;pT18w^bsigop=`NRI+LQml2WZZK9I< zfP_FDg7T>J!VOqmY3_n~$g;o3weADd_uU!as0R6f$nHKwQqkTU888@o(bUSbWQO;< z|KH6Flo4pAOTe}n;F~tX6YV8or8O=Cfn{qP+_|!Owh4tv6}LC!&DX0oejH9Ye@c+j zA>t4UYq%SqY}rSr%v7exc)(2Sk*KIkJ@WQTF{5C zugV46OE3PVmcPz0oWEHAsALK8!1(V)&2?3cyfQQKecmgutH`t71JH*@0y>(G7`MqU zHXH&k*8o4|B6h%sG2A8eb!&Jz0$3A`<0Be1fVIp9qxMj8b_QdIBQP%`O<%V)+nP6O zA;cz2fVrdt(4$s?9jkjfjak>h;bw(WD>)9F_P4-9RR*Ahhk^8MF=O$5C3y@_$0N z{*Y|}-gNU-N$kUAdQ3=$eC@A@v%qiuEs4H=lDU5(ogeQ@c&9Wipe-c-H>=0yR zF`?$vUN{t`YyPU=|XgEoEBd8sXIhSU@Y;pFgBa^VgW7>z*^J{9X(XYnIYC&F~K| zVNdsQWPqODQ>J8{)O&HxVIB^>ObQQd1vpDlz=4t5^D3K+hz*z`0A%ED-;ohf$Ff@4 zT9w>z=JU@LWQn6}I(xQ#B>#%#(bb zoxP&0<)vTpRSm;|uuw%U{OB+n22=ZOgSI5QO*w9unA^hBfN0Q6gg2&`Aw*;>wfdN} z5C9WB(s=LzfL!3I65b`zuf{_so2mnKI@%6nc@Fez#%HGIUwXccV>SHd6aJYYsM!@4 z7Pm2T>!!N6vJJ6w>a1>CKKcs!m)|9M#h!HjOmybf97#i~ddUDkY)+5cPaQf8C>0af3As}r#&P-Q<00$2s z!eSpONGxghQ8uthpFGbr_x-Mb3pnYQNsmxNYz8_@Zgg`t|z5(dEWw zl<-8C?kc@Z&IcE3j>+h4cKTRO52yUj&`3IVcRha1`IAP_j%;>z5*cL8w%g<$UXycA|jCn zhcoty2QR3R!LHTZtBA#ua60+Ks*^AvKH+ER2;F$mF?{0n@(tTSpZ^hsbbsEFQJ}P1 z<8>GHJIj-0bY3z%wLI07XH07zwbD6j$C>e0dB=RS>ywZO082-di zM0uV3QH`msXT7AT0?9cl5!Ge&{J8Wy70Jos*tri7q^we#ukeOiFh1sG5l zGt)H2BmZOgzi~--$ZoJQppUzQ*ccVwIG?|g#V#qPB&(8bVR9oju{qy_JMG}nsgw6_ z*?a81-u=Musl4)Zn9w<9Z{aBuxc$K`CGD%`Zx>^?jo1)u$;C2_s*=M->WNdXee<1L zFAtjBwWHZ-LCX%e(KR$Z;6ega$WY&3fOrIe+8Xns{qwAoy&~oC^c}xq0E#Qqebf*4 zHQw&2pZ`W5rsc?Hh5`5hXmk?Tx=06gC4|%K4?igm@RR+TOA#%#`u?S<0Cs4m*InHp zZknNbBx4oM;{-kXqpacPt8)$x=F9Ai+X0wq{Ov1vL8ouiIL%b3l4+nx=<9Gj@$t^F z>mic^?M0+;{nq*Yk_QFJX^%p8`zu$s4?(Y$LWS;y5$o#`pw&KsaWmb2K392VDw ziaFxY0erzpb`^tJW!!;cUxao)e+CSNggSq#xQ1fvOgXB@-q`5=n{2YICZMH4s(!}z zT^#`2;aH>-euhPSVcu>$uz8z!fS61ll6+Wjdb!lOH${9D5VJI!h`2JW0Z>kumuD{2 zg&z3QGho9CGG8#Tt^wAa$>uDYdEIjdFk}RmsN$CTCwYbrb)$iK9sc_?Z|a9%0F zy}+~5cfvlZgfw)C&79|*F>N=dSnJ({>Kk5mPZam???QnjBDpPf?iHuB4 zS>x^9w5@{92zD(&bn3jA>_EU_&-vnlK25G5CA&kK!C^o zK#DEoH!;|AADOZbnza+EM*xf%&3y!DF({CxG}T-3FfISf7j-mo+)59b0k{*i{4ce1q{ZKkrDsp ztiY+>it%)YfB^isZf~ljnTd%>djRV4^+?)=z2ZB8P)Wcwi{8359M1e2;|2z(yUur9 zX0Y&R7VbL#Rio${tZj&{fPg?SkSE8+Gw|-b9>brEFz0uGf#lwkbOZ2=!2nLTG>`?W zskv?A*DsS$2qb-p0*(u-%IMZ~*_&*bk z|F5P*z6E&8T#0`M7_M~E(_GZwiT`b&p|7zCbPKnJ?6*gfJXx75X>o?i8-L;qH#OVL zcT#7X-c6s&EjxoMThi}5^-BoN*?i(o*8gW?G}c_rCrWwQk}<(Qi8AR&i?WMsroyU% zCWA9-_u3iil?VZ`M@XF<@#)H(M3z;9fHzYw$*VPa+{D4=QzD<{Mr|d95D$69rpk{1 z3~YLTskD5mT!Q<#|4~00y79V_#gp11qv6-~TNgH7BCG&6_g@h-#wVE>0pVG>f z62mtPMl-f;vZzg^%A{?xdw48_VR|~$bvzhO5zsA=d}A2;u$09ydF35_26RBiFZuVX ze8+tyHSJ@}KbAnXb-!TmP)Bh;)@H6*`>EO%oWr8aH`_AvKT`_uGZI_ND(MFVvX*>HpVWxll+Ux43N>b*G!q(c&dpJuDt9wvq#gqKet z;pBx;XNbp8{e@ZoOI#L{Q85JaYYFsCTV8ZX7i4C>R^P}{@?Uco%0$enTYJ~)adR zU%3VWY%v(+{)Ks21n6N8bYbM>Tf#$T;o-JIhi~^hHMqY-n@Gu4at`aTv0-U0uCYf)kNM7?rSy%aE2LdYg&&g6h*t@20B4#6b*K$cah$ zwp$wuKY#wNFZ4$2iHO6;D$&tY-v~NsZdxuEAMD<_Jk|Q1_>>}E3Du%K+fZ+GpHt#} zi1j|_BT!GjTDYt2d2%Nl zfz-$+2ujy?cY+*~x1R)#gpZEtxj!iZ;O!0#EtA5JN1Xn{Qbe6Ex}*MTRYbVfw^%sIdyMr-#y@)ef4-q1f4UW zEwrXD1^GNz0S3;IYSvh*XX&`-gkMEvYO{z)*6y-4FS=zdqT;xVGR+RUt{#^30c-?2 zbg@`ClcEJ0K`61{dz3gt8~k3Y<^%zWS9t31N780}FutHaeQzj;LB#m$c@4QG3{_*A(iCq6eFSuD2 z{@ldE0-lf%6THD{zyTOu!?Iu0Deh?O?XUVl^j&dQ(0!q{S<;Ez)vG{!7O0!C$^~mS zcBSwjiz9aw|BXC%oYcjHIynLT)LBxK<*6Wi+vd9zgg(wWeb)R}Y%$dxPH0}eqMJ5> z7zkcu!=Qe~X-*|)mK1r%8zJ?sLC5JiAE;Y%ZA^t|M@2cZEaK%hRJN&aoQ3tqxoS;U z$~fAFD1p|6H}-7;9!wK-1`@kLCKG`kQAF#n@q$%}1gF zULdS90+M%3#fs9Qa#C+_{PX92ud11iA;UP>;j{QPzXcO&(M}-8X0t; z5e>;u1j3Xs{4I|=IRjF80P*rGRU-7DQaz6m%OIO^oPLhHv`L41K*FSC&n}(Z%_&gw z7*D}^S7V#EDg*0-H}ocbmnM1{8Fh$=Kr`)xajyw+5NW?+^WyhScqIF5y1!L_KZmTx zR0-E+u>R+}t*GxaUY5Ut@-=u*?QImYY`8gHLuCC*X=#~}uF@B(KTE`JDvi<_VtdaQ z%cTF-^;-nvC&2yCNU#mt3ufje)|`res)w9YHldFyO5a+Niy>U9RO;eY`XED!QC*uO zM{ugGhEZCja8!Q(*pBit<4UjZ5zKV=fs+F+_(4z)4H1;m1Bj=w?cJuI4(j#08-|KV z{fv39eSBQH^UD@xcMbUd0DiiY=T7)sBD_Z=w#HXS=0`Qc9Yc9ERkTOvTi0JSw}~0q z+#fI3p?mgW2_2L{Rq(70KI-#9_V!8+tCffc2xm|`eL+Uk zvY+=PE!s988aq1H*VM%N_o6qkZq9pcf)gjcf%$@6KYzK`{}j)~#-1!HJ)w`a4v5Rn z3{3{6C|Ep>UeA)|1aK-A!crpk-qY^M6B3Zuf}Vk5UfTw*mQ}NG6le5HSnjR9Aay0? zxGgo$l~=SFg#|ZYGb858KMtHDHe%`0k08xUxNX{=fk!yhv5Ox3rUdfK0X;$6jgADQR5TKF+e{@f(!?YW|rEg2F;VU9J@1%5R>*~lE-`LwyQp|*)oj)f80qJXd>yw;H0TuV(aTe)iDY^oQh1j8xjoEB+Wno+Wzt|#_8Rke9%mp z&UjwL4dY^A;DDg5t*vyWz)HYu@b8zwY9|QlXIE5J-Bj5f(nxSivFXoLFdi+@H-nkN zfzxILK-+tJDe>|81sh#y|7@Mt=lGH`GGJ*^&HyN*W78afsj#SB*QvUF`}V?5Uylz% zLOG%N<>ex{kWDnp#zP?GZ-2%wJ}=B$;%@{F_LfUdZ@%-LA89t4H+-lR>f%6gsBLfdgLz-z@KR~h2azFoT z!R^kN0&!)>;R{RwuGI1uJ))IHp-K@dej0CI?VnWvG6+e_`j%Vn4Ft|%)1y|tfgfmO ziU6JrR{%hM{j$kLl&iUWf2&%aDc2k&9ZnA$sS60HWN}Kpp*&=)G5> z^O?+=QEo1s^HRG5+ZP46g-!JAlGUvmVG$1_vk$H6?59cCa)^l~+*uV@6y+|Jlx>yw zSA-n%%K6eJ*4-pz^rWpf0Ntq&hb25`jB|f(yFpLyjK7S{_uqdVa63bn*m`uw2H4Im zCbOeSi{bqcs6lsH5sfbYByl%*o8$tG-}WmdZ!C$aP`V{3HOMZu6}5mt|uX{06>T}LjNtMUgJ9PApI;6b)?1JBdS(JIZiDH9v{RIb8!V< z9HP~LVCuOXVwOpZvZ(7Y)IZ_v|8wg*BLK&oWc{| zRHr`$Sy8IQwN4yWn=-JZPT?s)7J!W+6fII-kse@drt%cbKm+qNcq~dPp2wV;jG%iL zeHqYg?Ul-5+YTsO&N(nE1ZSAwKVLN-lTl}ByyStMNZl?tn>V-AKr-#2;K4fzYM`NbZbbJ*Gktf*qPcccp;;-7s63*2*Zg1QocC7~SQf`o zR~bZPu;B{G%z)z>K$I$mDh8xOklqA_5`qMXfQAwUfzb>G0-;2z^e!a?f`AQ>Do7b1 z2}l))1Q8Mx4D5^JY&mCkXMWo`Xa9kB&TIF*d++D_&6N0g_R#v*=QOY$zUqGfZIpyM zKs}PCIm@zU3DV`6D%v^`EJ7Yl9#Og~=~UP-Au-6L46R0|X-j|;fKcHHCWf`aQl8@K() zng;<*&s&1Yv3+Sy#-!^3Nlo1Yj53E9{Zj$@neduU(~&kK+3mb7e!|39jQjzMjvwp!;p}w!dTwor`)8EKGewM@P&#%|V&Y^J-cBoHP z#iFF2uq0tHx2c6%#~CQp3dBi$rYCkea1KMLMEs{FiI%~&xcx;pw#_W3u*n3g0#Q*` zzo2SRLW=&gFagNvQtN;;*pn@m&@0W?-7XShaFfR`AVuczz#KpB&(?>Nt&z5254l`r z8rsSkVO97QD(;4BZdhBG+;YDQ^4$!1th(}XPWh@^6c4bZP0&l0iecyd>o23Q;o7)^ zifb?Csn4`NH-IFbH4UAAZm9@7qw>WRg3dG=iv`av^r})y!c!>~Vh2tc0MVu)VPG0h ziNygn&bC=lNm2HR&mM*MkV7K#7QAc=!&<=xXnopRUL4Jdu7FakqSnj^&YM0hkkWE$ zu>TKH;d|bBVS{`xy}F4l>+h{SU_hu$vBTq+$^@R_GnB_OoL9%)SA&b(0f(j;pB+hn zfGFVS^bV`FK*45tX-$XHU>0M^@H`;%$zI=1ffy(>rmj`eki-4Ec&*3!2CDa@)SLMj z8x>}!7q%`XYoIuKxLL`3N3-(eN!iD`J2d_5DU6k4&(oih(nJglyH43$|3>xf4wB%` zOTS3?9F{s{&DKfP)qCG`=}0eZmXF#erRHQg|CN4)yE#b_=Y^#>3Gp0PjSI)#qHk(# z%D7J#3A2pMT@gf%1Y`)D8Y>_LZ1t&;SClU07 z3k_x@j{3se{ZUSI#ml+;u+9MnEbCKzLFfe6Wb)Q=Y6K_+oj%Nj zEqf;7bxn{<$V|gg({v%8hLhP{E zY~lL%Z14D&e2alp_i6l|YXY^~Tq;^>XsKd|kZ0q!FxS^M8~*g|q%L72MBwt~8Ptib z$ubtWn%;A-xMOUNc_^bQN(zYZFPdPW1}W=^Q#Oh+?tn2kDz^KC)%o>=iyC1;{2FVq zOu^6);A+%Q{C9JKn&jO}#u*-|v>INQ?2_1zCi5e}Z403)iwEYJ6|O5!HoVzYI5&Kq$Px3+je=x3I?=|_1Iy(J-EqRf zUqRqKjEG-}(5;CDu3L3iJ&4zQ-hK)Kci~s8f7U`^!bD7nKxV?t=qVJ;5*Tv6- z^@L&k8mP4#a!W5DPg^?-2_;fp#wF!-mMmbB1lB+8Bav{K={4?I)8R%|6GRq_MSw99s3_DAMMhC=_k}WH?HAig#gm3mpq>s-u z1l)ff&l;Qf$9UF4L;c_6Sz}h8dmVlDW0eS_#B+EmQBoY0yE(LBOiXe}>YM!V?Hi&z z2O>qeAnZIenZyy&9%w%m?V0UjoZYtXZXbv;^ZpUh=K+*3-Wg@6=~!U5F5bwRDzH&S zvQZ7y5kU>WnGNX@TT_D_tjM=1Ps4+#2-NuHC3Og3gpkR-Mv-2)+w6K*E3nO%zScAL zP{w6i-ZyutxOHqR)(iUq(-4>=w+~GPiSpF4UAZ9Uc9DK4cJ~Gq5rVm+X`4_2!C4LX z1%{>0{a139W&L~PZ2G}So>i69=Ivte%K>^03cR{=aQOvn9`#ApPDJPU{*Z3cHdjZ^ zw7V~!_2fNUiP`8s4&72Kts`q{7aRp%QAy= z;mWu2f_^O2xGCO*{q**5F{$ahp`5rn(@3Gdc(B9t0EB}a?YeCjwgYZqN4Br?{~0Eb zgSlpWvXzWhM?;k~FS=+A*_g?W6t`>9zeNexfXA{x6vbM6cx@(T)j{U@gp<51J=Z4r zttAL$ZuTGN%NwVFOoARG+U7G~HOZQLAnANG8+=2{J;a2W(~_@d-^`YjZbUO1T0$&d zNM;K1(BlS}6^1jwfRxt$t$f;}@uRnIC-6e+TOQpY~_}fKl#mW$_w* z+{%_M0DuMFnVOnX3Y@uA?%Xt%ovC8>#n%V;yA}X{(gl#f4g+z;V7`&4N$MFLH8nL; z<~GnLVdisUl`w1G;VZ48)Yp-M*VY*!NYMY@v;OGgH#fney)HW&%JGepfLiD3#lZ_r zGkDO)4GhKyFb$^6m-ki8bYu4Qms7?|?nyuww>Fn;D1Hnj>{b{rkFQq==HDtSDKP{z z64;>Tszy*NF)#p~pVNFiA6VcQgQ-1WGQ1)MG)d^Vx~+_L**OOA5+OPAA?`-;+-{A*^G%|MK-LpY1PbY_ z4oXH!%1K=713)vxzW)c(FgiNAS3scHN(+<+*3SP_AgYZ4nLUu*0n1qp+?_j!x2Lo3 zygtUWa7sVCbJs2pmHS91|IEZhOJwqNQG9&7eT_tWDgNLWbl_j01!qsPJux-a_6;Qc z_U*rL*9Oe?djL_JCGfG;4JO9YL_)xs?Zm~K-ZKApZoqJK`*wRQh`*6ns68m)|; zGX}#HgUur;Ba_1otn~C^93Agv0|@Lz2$A$BdDYg**zRzrR0@Ea;{F zrJ@Dq%a<-;8wpnOpQ-Gh{Z9Np`A+}2=Gwizi$=b}QK2LLgkD}s=69oP@=5G-cj!Hc-3(iKlV7dua%tM0al##K+(8!nzV9Iml?+q!!=xHz8| z5D^eQ!+PD*)73*lQ1IqI7YMkx+Y9pF8My*CIq0ft?13QESJD5IypYXyKoE63tl}ko z-{hHr08ewTl-=DP`?Slg*v`KmT@?A!A=h~=M!VPjSf&!|fwO$7(_-86~x;5MiZGPNzWsanAcFEBd`KH=Jb;ONlNZGILnFE0`; zVd3NV?_Rvwo7vlI5}%MjA>De9w{DuR&I$jv;MxZ-y1xWvRx5*R+!EgO?}~H!RGr59 z&%dLfOSwWfZ&SO#E(eB0sa_U%vuy6Q+MCjG3s-r{sHCGm)oWFU**w7#N%87jC97%9 zCy%q7e^h{{c}On7a84Js$57H3_>=F#wVQ#%Vf}}K7K9vT>)uiA$FVxU1FPjh64jM2 zKRVO?*O8MP*kRWzM*aR35`;Fx?lrPhDvUU4=yD{k=0@4c?h&aWJAPpwt`2y#Nt?X6 zkth*>cf8@OCxwsJ+b8Ku;u2>15?yEQTenfqxvI6PCr$;7_o%VCCs=xJKKSwTuCG4L zwPc;Jkp@cirnfM%!Ii=5A6V$?j5%vxT_Xo?VMUs3bm#oq0+xSfL{a&rtjp!>J^Nx_ zNGTyZ)nAQeXFj~dd7wO9h4havHfAWEep>f{f%=!XEiPudoJ5O!j2d;vXzu%pBQNv4 z=gKCS%>!mi;E~6U?frq`LtVpw4^jGMAKf{_mxt&Pq?RWjBc6JM%=#EVD?CBQy>;ZT z({k`HWgTnl3=7|3a_mP6)?=*jPKM)nbA0(qx2b`lp>iNH!ia9M4&8ml<+8RQP0>N$ z47<-y=$6IghOUr6sK8vhuP9%$UMKihCH;NHd(`RFd*8rrGfy^g^XARXEB2wB(D!?hgehB8!p60?*nvcAKP@vFREES{TPa*U_m0~x5NmbZnN;RNcDLxF8tzjM@wid# zy(wJ(^GA}4ArGytk`nm>2^0C={SUk?AZYVsYV&aLGO(e0e&-Y=k9_}aas-*D$2Vot z3tHrvI+rO`&rwKIX@A{&R9~9yg;!TU?7tu?%1{4bGBsfKp#Pcl>#s;*HS_!TH-yLa z%}f=w2psa|OMkn!mme?!GT&w>#e(Z6YGy#qn)JYGlh)KB%XQKP5*#V|lsMia8fP7Q`wL!>6`C zJlnxns-b$MSr6&J0?*l1`gD(79kuT0e?T!vvTSoF3zjcBUbtAABN?w_d*<2rl9Nc*?RaXd;$y6} z=kiL_IF4V65;~1<8jFs~diGDm)X1^u$uAwumEE4y>a7YWovmEbJZoH}i0?W((TZ6v zc)wFYG~w^`aXqd530DHacb~SZiHXTrx$_rm@W*e2!s^@KC?pYhTSL|}3S25Q578cCQ7D9|@S}Mn+OeuYR+BA``S(+XHXE-KNaCebbi&wnvpK)}+$+)x{g%(&4JAwW_)J z3BOBGvwyJE8;J8X^W=7u?#U*RFprvIRFE(nf8G z$MGYDwi)W>+Qkq$)=}q3=gHFL9U>nJG!{J+{_$L4 z`mRO43%L@*`?l8`WV;(=R)x-5Rwu6`5&O`uqr4kS$`$GO^{Y%ssL8Edks9Dekcx!q;EqF_;$x`?M9*Hw}P1fDxQTeM)8g= zEkW%E9qKy_p7?f?muWrJ@!_NCe~Cn%*n6_~PpOk#LT=Wq?)*x#UA@s(^;A%X>vNUw zP(Yy51vdJF*RLTBx6x!JcS&sd*3w{j^+g^Trg%%g&gk{%%$K9rH!7d}ramHaR)EW2&H7? zl)aIYSCIKj29lu{XozGzx?O4(c1oq%#g?N9^Zm78{6LMV-E`Xi)D2a8X)k7V2Y=hL z0&%DEM{R}eag>HKq1P1A4XZpESRc+2CLfu{!QON?ql2?0>$PC#+tM^nEZ?DUPc_@J zL7#5VAoupWn{raY>O}5RjRMt@uG=Ti_OkivT_`{z$`|gMutAi zVgdhnvyk0T^gE-F#S3+Bq}U~YO4mQWrP{PvP5(J>L*n zEhuQcvUBCt1G}YKacz0j3!;r6nTiIxPDZa|0^j?IjJ@y=REOPO*{uw44vi(n96_C; zF$oX>=jBJ&Km2tB&%qt{AnKskTpY(nw|hN>_Q>9S7{cpS#F)CJ*ZMP*j4bl&e_Vv1 zn~PHt8ztj$+l%5}sS}gs(*=@eWFDP0<-(Rfd@UU+bjKV_ruN18`k#=-^f-=-pOHau zXCd$)p~t{R^sPOT%5@9i!6Eu)X@a)_abweD^=SZXsx+{^u}5 z=`=o(+rcX+MP?;Oi(Hy3G!#Ajp|{{FUY#{ESqwfb+9FIHV=vm@ux!WjX{O}sJ<8MX z{Nau5ICXBp4y5>yA2bC$Hs94kz354mpA{99T`&g{e*stTEBZ+QZcK11Zmey)WQ` z*plxuqy5coY{Rg*Jp5xgyZIYkI(zfSTVFA-gCv@V(~qL3KlE7SV(IvM1w5mGjDR`s zX9=xS6nc2~>}0KbO1Doi$lV%3VUw#m>=vZyGKUqJ<&qC0A3n)g;>Y}|Ie34GnaC7! zr8|(&QW}Dp)C%R`l;W}3#p63|{8beicpK{b)cm}(?r|$YXP+3?k^Mx_V|BgpsEwfG z*g&7PHrrY?bXR1yImy;8P0(qkb}P4`!9cdcgfKl#`h-@FZI~V)g{$JlH*-CwYJw~% z@CBOpR93PGKc=dTx~CZiD2k^=sU3T}L>(ca#<%)UNj=uBU-J6#Rc-tFhU5h_&mnuz z*jLZ_BB5n2$gFL)pPN|fj>~w-o=+?{wNDe)>48kK@_M2E-TN>l*8~&3wz2 zrqC#j^Y&LOwN|GzZyFjC(Y=sq)FGig zwzun7EhHaz^fJ6@X>PRXP#F1eLZvB;KQ2XBp}w?vv*T9g$@z~+1UZUlhtA0oUz#W8 zamNPTS$|w6hqnQD>p8e^J+zaQhYNKO1gmSWrAsaITFKO>^0^T{@|NNBks7l#Vvz2?qY$v;I&Epg@xCL>m$O!@h>XL+MMO9dxsC6`X+-usKr8^>f# zd$Gf>lY4SJq*Al}oTxq`=KlL3I1Z-!TR&GDoH*m+=G)2F`dd$HdJ4>E)YJ?001df{ zW!~u41D`GN>RcJ;ToM~+s?|Z^#TBP`RgT-t)`}dlO>8WemHq)H<!M@xca}&dhH1xH$HP%MyI01h{GO9AO+mBM}}KvQE?Y4>jam z39-0ybD;df&tsv-E&*mcSO222QtGFWvc43vV3`wMZK+&s;Ry%SAC+r^4a@xnWR#5T z+|z0`<$6yqO$266O+lSUL&-ZP*1TCgc%j1Nd@dKc)J7xEo3tLQ{@^g(cd-BzamP;f ztxcDm+M*R!z8L$cJNUAM4PjuAap~){-FRRBok>U=CFQB6!3d5CHFgXc8Vl%%F1kK^ z>-ZXM&n(T-de%+Q3L;K+F1##x?+(S+V7Mg(wW-nHAHh;D=*eo%5kW}=CK;#-B2wLgblFbJadmE1>W(?3vEI594> ze|D=|Ozn)>)8)xSV@G;pWNmQH(gAAKD&*(>LW6G{rfqHIzNJjYk&24&O6L}C9=DCN z&6=rCKfb4HZZ%`Ys@YZcuNf1{r_??tH|855%G9M+B#5>=l=J12mT4WE`4!>Y<@t|a z3=&*olfTqDRi~Mf7@yl#++N&#@U?tAtMU`2)s54zYdj>Wb51K6pEt*s`icpXIk>sY zj7HT>}B}}?1#@;+V*1<)ZwB`CW$cPIU847=Y zT4A23Zt~o|?IlriZS20p>nH0-v@CimN~mX-2;NB8^ez(qUVJSoPv`mkdD-Kc4nqq%r#8Wr3g|l;;`*Sa)g5ddzoqlXjKa6USMaE>)Nxx#%;suO#GZ; z_4}hOf9I1Sa>u6C$|A%f^OEqvUfG{gFNGhrD3bU7QATOp)XaB+GDqq}2Vm~aRWF8p z=yOJ(&w0VOY~IgJlQtksQ`ek2 zge;;lF#X<*8^l7%;H^q34)c_wQB!mDsZZy&iI$N0%>6T(?b5Pe&Vj{k7?!`Zi#XwO zo(Cs3 zQi$)6_mF4 zZ*}2c&*=faL{FHW@_g+c%Ny2{AG1lt7&oe#5U9%aq@u35)R6mQ28reoMCC8Es-{#2 zHsQ&|(w5%-=K37EWAVFopQCA7y(S-mXP;bWZomXh=9XQ0cozMru{XM^`V#fSnPIDzpc z$J-~5Baviiai+~0NHpqNe=CsahcBuYq@i@`#h(oSsM5Vwa@{UIx8CEuc<6{CdLaT7 zXhG$oTF`_fv3+V&qN}Z-Rr*lJTDspZ_UB$at`-<_AQ}4RhH`QwmpP7`6xrNID`~VC z1@_A4_Y4s|_J8ntabWPn=lU-fN&XN@`>wkNs_FvZ{+gr5i8UAE^5DQb|2sEuymfSS~WywWvQCbM=A^6A| zSFV?s-asB&IfSyp%8!#iJ1zv(^ejoP^2RL0l^AxHN<@2Js6O+o{7}KygDy34?aeft z<5Y~vM<48yx`2#PplA;FvTkkLB4n`I$WS-2DB{L-ymg*BQB9>~w&2!3<#Fu~^3cH3 z=#yUhFPYYO>umh?LQh1&?tPme@kkvliHy`Zek|l_C=YeJd^N==&Rs;01ecCRw?MS! zaM-I;II+VCY4P7{ObziR9sFDVeo?~nO!tg)tdj~^< zknWLZOx#ZSh6QFhM^0W;#lIhZsaZB_Smi&GxR@Vr{i(Kryi@C|i*wnTq5CAPVyGXg z;+!eV);GX89&I@l6{c(b9#K$nK7Tl~<<;Pl)^jV9aDU-N zb+K>34|Usdvf6LTp-{No{WsD1*udwYnE%2}=bn(H9I^!x1j7t5y!P17Us%T= zr||cY>}?u^l^wzB_M@U>75~tkfD}lyD3P#-rZ4{|M5`~+vodtvbj(irJC4vzQJtyWMEdiDh(xKPRn-GJK|V?HJYLlPy$;fA3}NWQ#lKe|YFEqZ8102hyX9 zi;Ek*2GSc4ZL3))KaG&yYAwm&)eB{(3V&F5%IB7Ea%qSy-M+PUR?Hz;#zd4eomXe- zS^JBrsj1t{WMTq&05Jb2QgF)s5B7$YdQHvZg11+?!s}GJeqSEDE05)nSNzBYY@Lt!Kl~e? zfL4w3`lng|gYr*;2lDxn{34UJRoS@LRBbJIfc=*+#> zV4L5SI#>*$O-kz^l65B*cnfR803&%~h~v`qLW~>TZXrv{GBakeI`1I==!S4T&-B)l zqYuVHt{^(iRv?VkE71)5XiCr@$ zN!N$i6e`rP6cjX)(Dt**NWwPj*7||DJ~phouC)8=%!x(mZj&aAxktCin_lwJ(c1Iq z^;uN}qq*`4p{k;_bSVN*87EXww5cKM(YVIQ{yFI)_J~m%wUr~na|&IL`utvj>pwJk zw)cBVTu|(KW2@P@v4Dq}bcZxMsduKH-O_6#K^jn)j5~;y!WF{?C2M0NADEQ_&GgS3 zQ~zG6BT+wsWT9Cku5z{DF0-#4p|>Koc~c@cKFa6Q8AXd^0l0VG5_jKdRZHe@9Vyt z0$cZzw=2d{0_$tHPFd4>Kb2@DUhu$H)15JF&eeL zt6JnXq`rIaVN?P`h_nnYb5T`wDLd^&MS-*+)a_Ph(A3IVw-oewXTs8<;nv4!h5ZTx zDN|L8F5?DWs#B&18JB}zz08QR6uWF$?Jr|GLt4u-;&KIedDwA=0^{r%KqkPFB<2wo z8)^h?&*R+}h9CDB$)r}_d{w_k#>sL=h!zK6nH~ zXCq+TO!;F!GtsjE1v7T)ILonypY2f=&%@3W?qE(L^&;TF+Hnw;firpPN}LLNRsAfg zpY!~u=J59E3JXT;L<*Yq5R$8(8R5bcNqu7*g)W(<2Fx+y!yMQTMDMQMRtN65X<0+_ zm^K{}7p*Uaus)kFuESWQdT4Z}!x>f{#HCC$_q+0Lf# z&$A#$S@#wJ^rv67EC-ikE*Exg<3+hHV^|OY=RyZ{hVHS>zv+;B)W`vp4&_nnN!7`6 z30$;G@!c7hSV&gFb6DdvCEY4JjI~lI^cI5%NrErB&N8%i#6|el(vTq>=$CN(-FXH? zXO{EPi2G z)>SEGvKA@(^s_5u;~9SDCZ40gjg~TO+;p#6;`=35w*I2Vdlh1v+@&d5oB8mz z8wM&`y7d@i(R}Hsf|&2GrPsK)Gx?3ST?RH14bWD&4_`*}6~&f`9@iE+gq83=1mUfz1?QHi*-u< zCc-1L7p|T{omrlc_vLM=<%_&I!mF~gZ*Rf%XrV|OhX&2HWqQADZi>}96<_VZO#1xj zfZo8FDa~df<$)Y^S28jrk`mo*3?wJ%>N4~DIMI2Nv3PsB-kQrD)7;4w*@DqFUsK%2 z_qvL5u+4j&3m%M50rAC;QvvZSGqp~My-V>0ganodo>#76Mduk*@qtd9PPBh|jbA<@ zp&4}wKatSdvhR8H#I*qF?Pe#nfSoUb5|JHC^ey?)mT%mfqXiA(!ix9mg4d(4Lx<4# zajW9@V8cqdk%eCOCXin-38^PG_cs8!E+4u4&hJ9<3JIcw)?;#3QpBFKyPF{^;cukz zF?m~?WD_~F(rRgI)mLlUVwT%|K6Mxo1Dt~?_9IN9dv^w}pW6wJ4essd;>XT@87OGB z_^p*VE3Hu7pc)~e{J>GGqm&pDJZKM}klp*l6_g(xcxBYB#x*H4{5XxZU}qkT}QA9S?>hf`mjUsqk7@wSaSM9t{XJw}ol{r1mfDKhLh0M;cqgTOm^5~KZpLJKyVmX!?`Ib;#8i?j`9qJ zUKvAPp__)R*ssJWh0zbMI&M$KE+?4^9I|soYVVC6lz|S0`|Ik#iq)*1Q3brsSa&M- zNdeG}*hbQNb2N;}47xf$O#S`wn$m~g)rS!^@{kO`7F~3*>s70nRN@2u7+c4-1RVRc zSaWKs>BhW`h`@j~ko9a9J&KJ~g|L9vB9gVO2ol&UL z$Ql`Ar;(TrSq)?xNZh&Ll6?q9Cv|Sx+-Fy_?M5HFB{bZDMVlKa_!;>yVz(zMe7pL3 zqt0kV*hdN^PR#KC%)REAb;?Tp&P)CNMx}ddWgS!8XJA*~f3?QN*(=MDuO;F~n?=s= zN~$_9IsvDAoY18x5++gW{E|!5B((TPkLb$^PA+5Q+7|)|6={9>X)c3;Jqf?gEeQD% z2HtTsFy#jkZzz2@^NGxb>%_xc=sEus3LSk#b4P4NXHk8lO(&o1`}G$EJe|+m%<`l4%xb=$T}|sbIb2TJ)17a6 zhnWNe80Xw!Yg}_n%N-z=>qZyw6Tee z`AV=G0^#8{dSH?F|7m4aRc5OUPpY_LErQP<@c=*MFrrOT}dUW1WM8aW+{$i@&Q0n&#)x=zJ1dw1NJSJe!KBr8WUT zL)1E8Gc=Jh#qZe5SPXWuH?%c}EV5}>23Bd_Q-X$UvuZ?L3*AEgv#Tnj&#u~@FOYF< zb=d3%`c7H~VYLG6G;|M`qrRQC^P3xg8Fj@rH+ppPg?K*vGJ

xc6tL^z`7Twp{v0?)k2GMI#wD$Aiu&|*4Xvx5 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 literal 0 HcmV?d00001 diff --git a/tests/test_contrib/test_prepredict.py b/tests/test_contrib/test_prepredict.py new file mode 100644 index 000000000..22ea7efe7 --- /dev/null +++ b/tests/test_contrib/test_prepredict.py @@ -0,0 +1,183 @@ +# tests.test_contrib.test_prepredict +# Test the prepredict estimator. +# +# Author: Benjamin Bengfort +# Created: Mon Jul 12 07:07:33 2021 -0400 +# +# ID: test_prepredict.py [] benjamin@bengfort.com $ + +""" +Test the prepredict estimator. +""" + +########################################################################## +## Imports +########################################################################## + +import pytest + +from io import BytesIO +from tests.fixtures import Dataset, Split +from tests.base import IS_WINDOWS_OR_CONDA, VisualTestCase + +from sklearn.naive_bayes import GaussianNB +from sklearn.cluster import MiniBatchKMeans +from sklearn.linear_model import LinearRegression +from sklearn.model_selection import train_test_split as tts +from sklearn.datasets import make_classification, make_regression, make_blobs + +from yellowbrick.contrib.prepredict import * +from yellowbrick.regressor import PredictionError +from yellowbrick.classifier import ClassificationReport + + +########################################################################## +## Fixtures +########################################################################## + +@pytest.fixture(scope="class") +def multiclass(request): + """ + Creates a random multiclass classification dataset fixture + """ + X, y = make_classification( + n_samples=500, + n_features=20, + n_informative=8, + n_redundant=2, + n_classes=6, + n_clusters_per_class=3, + random_state=87, + ) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=93) + + dataset = Dataset(Split(X_train, X_test), Split(y_train, y_test)) + request.cls.multiclass = dataset + + +@pytest.fixture(scope="class") +def continuous(request): + """ + Creates a random continuous regression dataset fixture + """ + X, y = make_regression( + n_samples=500, + n_features=22, + n_informative=8, + random_state=42, + noise=0.2, + bias=0.2, + ) + + X_train, X_test, y_train, y_test = tts(X, y, test_size=0.2, random_state=11) + + # Set a class attribute for regression + request.cls.continuous = Dataset(Split(X_train, X_test), Split(y_train, y_test)) + + +@pytest.fixture(scope="class") +def blobs(request): + """ + Create a random blobs clustering dataset fixture + """ + X, y = make_blobs( + n_samples=1000, n_features=12, centers=6, shuffle=True, random_state=42 + ) + + # Set a class attribute for blobs + request.cls.blobs = Dataset(X, y) + + +########################################################################## +## Tests +########################################################################## + +@pytest.mark.usefixtures("multiclass") +@pytest.mark.usefixtures("continuous") +@pytest.mark.usefixtures("blobs") +class TestPrePrePredictEstimator(VisualTestCase): + """ + Pre-predict contrib tests. + """ + + @pytest.mark.xfail( + IS_WINDOWS_OR_CONDA, + reason="image comparison failure on Conda 3.8 and 3.9 with RMS 19.307", + ) + def test_prepredict_classifier(self): + """ + Test the prepredict estimator with classification report + """ + # Make prepredictions + X, y = self.multiclass.X, self.multiclass.y + y_pred = GaussianNB().fit(X.train, y.train).predict(X.test) + + # Create prepredict estimator with prior predictions + estimator = PrePredict(y_pred, CLASSIFIER) + assert estimator.fit(X.train, y.train) is estimator + assert estimator.predict(X.train) is y_pred + assert estimator.score(X.test, y.test) == pytest.approx(0.41, rel=1e-3) + + # Test that a visualizer works with the pre-predictions. + viz = ClassificationReport(estimator) + viz.fit(None, y.train) + viz.score(None, y.test) + viz.finalize() + + self.assert_images_similar(viz) + + def test_prepredict_regressor(self): + """ + Test the prepredict estimator with a prediction error plot + """ + # Make prepredictions + X, y = self.continuous.X, self.continuous.y + y_pred = LinearRegression().fit(X.train, y.train).predict(X.test) + + # Create prepredict estimator with prior predictions + estimator = PrePredict(y_pred, REGRESSOR) + assert estimator.fit(X.train, y.train) is estimator + assert estimator.predict(X.train) is y_pred + assert estimator.score(X.test, y.test) == pytest.approx(0.9999983124154966, rel=1e-2) + + # Test that a visualizer works with the pre-predictions. + viz = PredictionError(estimator) + viz.fit(X.train, y.train) + viz.score(X.test, y.test) + viz.finalize() + + self.assert_images_similar(viz, tol=10.0) + + def test_prepredict_clusterer(self): + """ + Test the prepredict estimator with a silhouette visualizer + """ + X = self.blobs.X + y_pred = MiniBatchKMeans(random_state=831).fit(X).predict(X) + + # Create prepredict estimator with prior predictions + estimator = PrePredict(y_pred, CLUSTERER) + assert estimator.fit(X) is estimator + assert estimator.predict(X) is y_pred + assert estimator.score(X) == pytest.approx(0.5477478541994333, rel=1e-2) + + # NOTE: there is currently no cluster visualizer that can take advantage of + # the prepredict utility since they all require learned attributes. + + def test_load(self): + """ + Test the various ways that prepredict loads data + """ + # Test callable + ppe = PrePredict(lambda: self.multiclass.y.test) + assert ppe._load() is self.multiclass.y.test + + # Test file-like object, assume that str and pathlib.Path work similarly + f = BytesIO() + np.save(f, self.continuous.y.test) + f.seek(0) + ppe = PrePredict(f) + assert np.array_equal(ppe._load(), self.continuous.y.test) + + # Test direct array-like completed in other tests. diff --git a/yellowbrick/contrib/prepredict.py b/yellowbrick/contrib/prepredict.py new file mode 100644 index 000000000..ca20f1288 --- /dev/null +++ b/yellowbrick/contrib/prepredict.py @@ -0,0 +1,104 @@ +# yellowbrick.contrib.prepredict +# PrePredict estimator allows Yellowbrick to work with results produced by an estimator. +# +# Author: Benjamin Bengfort +# Created: Mon Jul 12 07:07:33 2021 -0400 +# +# ID: prepredict.py [] benjamin@bengfort.com $ + +""" +PrePredict estimator allows Yellowbrick to work with results produced by an estimator +prior to the visual diagnostic workflow, particularly for inferences that require +extensive time or compute resources. +""" + +########################################################################## +## Imports +########################################################################## + +import pathlib +import numpy as np + +from sklearn.base import BaseEstimator +from sklearn.metrics import accuracy_score, r2_score, silhouette_score +from yellowbrick.contrib.wrapper import CLASSIFIER, CLUSTERER, REGRESSOR + + +class PrePredict(BaseEstimator): + """ + The Passthrough estimator allows users to specify pre-predicted results to + Yellowbrick without the need to input the original estimator. Note that Yellowbrick + often uses the learned attributes of the estimator to produce rich visual + diagnostics, so this estimator may not work for all Yellowbrick visualizers. + + The passthrough estimator can accept data either in memory as a numpy array or it + can accept a string, which it interprets as a path on disk to load the data from. + + Currently passthrough does not support predict_proba or decision_function methods, + which it could if it was passed predicted data as 2D array instead of a 1D array. + + Parameters + ---------- + data : array-like, func, or file-like object, string, or pathlib.Path + The predicted values wrapped by the estimator and returned on predict() and + used by the score function. The default expectation is that data is a 1D numpy + array of y_hat or y_pred values produced by some other estimator. Data can also + be a func, which is called and returned, or a file-like object, string, or + pathlib.Path at which point the data is loaded from disk using ``np.load``. + + estimator_type : str, optional + One of "classifier", "regressor", "clusterer", "DensityEstimator", or + "outlier_detector" that allows the contrib estimator to pass the scikit-learn + ``is_classifier``, etc. functions. If not specified, the Yellowbrick visualizer + you're trying to use may error. + """ + + def __init__(self, data, estimator_type=None): + self.data = data + self._estimator_type = estimator_type + + def fit(self, X, y=None): + """ + Fit is a no-op, simply returning self per the scikit-learn API. + """ + return self + + def predict(self, X): + """ + Predict returns the embedded data but does not perform any checks on the + validity of X (e.g. that it has the same shape as the internal data). + """ + return self._load() + + def score(self, X, y=None): + """ + Score uses an appropriate metric for the estimator type and compares the input + y values with the pre-predicted values. + """ + if self._estimator_type == CLASSIFIER: + return accuracy_score(y, self._load()) + + if self._estimator_type == REGRESSOR: + return r2_score(y, self._load()) + + if self._estimator_type == CLUSTERER: + labels = y if y is not None else self._load() + return silhouette_score(X, labels) + + # If the estimator type is unknown return NaN since the score can't be computed. + return np.nan + + def _load(self): + """ + Loads the data by performing type checking to determine if data is a callable + whose result needs to be returned, or an argument that supports from disk + loading. If neither of these things, then assumes the data is array-like and + returns it directly. + """ + if callable(self.data): + return self.data() + + if hasattr(self.data, "read") or isinstance(self.data, (str, pathlib.Path)): + return np.load(self.data) + + return self.data \ No newline at end of file