From 9302df86f30db4e3fa171dbed00e97c1b7951185 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 17 Jun 2024 15:05:19 +0300 Subject: [PATCH] Add bounds parameter to geom_density closes #796 --- doc/changelog.qmd | 4 + plotnine/stats/stat_density.py | 85 ++++++++++++++++-- plotnine/stats/stat_sina.py | 1 + .../test_geom_density/bounds.png | Bin 0 -> 18758 bytes tests/test_geom_density.py | 17 +++- 5 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 tests/baseline_images/test_geom_density/bounds.png diff --git a/doc/changelog.qmd b/doc/changelog.qmd index 88198c4a4..04124c4dd 100644 --- a/doc/changelog.qmd +++ b/doc/changelog.qmd @@ -18,6 +18,10 @@ title: Changelog - [](:class:`~plotnine.geom_text`) has gained new aesthetics `fontvariant` and `fontstretch`. +- [](:stat:`~plotnine.stat_density`) has gained a new parameter `bounds` + that you can use remove asymptotic boundary effects that arise from + density estimates on an infinite domain. ({{< issue 796 >}}) + ### Bug Fixes - Fix layers 3 and above not to overlap the axis lines if there are any diff --git a/plotnine/stats/stat_density.py b/plotnine/stats/stat_density.py index 54b615963..d05c2635f 100644 --- a/plotnine/stats/stat_density.py +++ b/plotnine/stats/stat_density.py @@ -1,7 +1,7 @@ from __future__ import annotations -import typing from contextlib import suppress +from typing import TYPE_CHECKING, cast from warnings import warn import numpy as np @@ -12,8 +12,8 @@ from ..mapping.evaluation import after_stat from .stat import stat -if typing.TYPE_CHECKING: - from plotnine.typing import FloatArrayLike +if TYPE_CHECKING: + from plotnine.typing import FloatArray, FloatArrayLike # NOTE: Parameter descriptions are in @@ -77,6 +77,11 @@ class stat_density(stat): clip : tuple[float, float], default=(-inf, inf) Values in `x` that are outside of the range given by clip are dropped. The number of values in `x` is then shortened. + bounds: tuple[float, float], default=(-inf, inf) + The domain boundaries of the data. When the domain is finite the + estimated density will be corrected to remove asymptotic boundary + effects that are usually biased away from the probability density + function being estimated. See Also -------- @@ -115,6 +120,7 @@ class stat_density(stat): "bw": "nrd0", "cut": 3, "clip": (-np.inf, np.inf), + "bounds": (-np.inf, np.inf), } DEFAULT_AES = {"y": after_stat("density")} CREATES = {"density", "count", "scaled", "n"} @@ -165,12 +171,12 @@ def compute_density(x, weight, range, **params): x = np.asarray(x, dtype=float) not_nan = ~np.isnan(x) x = x[not_nan] - bw = params["bw"] + bw = cast(str | float, params["bw"]) kernel = params["kernel"] + bounds = params["bounds"] + has_bounds = not (np.isneginf(bounds[0]) and np.isposinf(bounds[1])) n = len(x) - assert isinstance(bw, (str, float)) # type narrowing - if n == 0 or (n == 1 and isinstance(bw, str)): if n == 1: warn( @@ -211,7 +217,19 @@ def compute_density(x, weight, range, **params): clip=params["clip"], ) - x2 = np.linspace(range[0], range[1], params["n"]) + if has_bounds: + # kde.support is the grid over which the kernel function is + # defined and the first and last values of this grid are: + # + # [min(x)-cut*bw, max(x)+cut*bw] + # + # i.e. the grid is wider than the ptp range of x. + # Evaluating values beyond the ptp range helps us calculate a + # boundary corrections. So we widen the range over which we will + # evaluate, so that it contains all points supported by the grid. + x2 = np.linspace(kde.support[0], kde.support[-1], params["n"]) + else: + x2 = np.linspace(range[0], range[1], params["n"]) try: y = kde.evaluate(x2) @@ -235,6 +253,10 @@ def compute_density(x, weight, range, **params): not_nan = ~np.isnan(y) x2 = x2[not_nan] y = y[not_nan] + + if has_bounds: + x2, y = fit_density_to_bounds(x2, y, range, bounds) + return pd.DataFrame( { "x": x2, @@ -277,3 +299,52 @@ def nrd0(x: FloatArrayLike) -> float: if low_std == 0: low_std = std_estimate or np.abs(np.asarray(x)[0]) or 1 return 0.9 * low_std * (n**-0.2) + + +def fit_density_to_bounds( + x: FloatArray, + y: FloatArray, + range: tuple[float, float], + bounds: tuple[float, float], +) -> tuple[FloatArray, FloatArray]: + """ + Fit calculated density to the given bounds + + Parameters + ---------- + x : + Points at which the density is estimated. `x` is expected to + to include all values of the density grid. + y : + Estimated density. + range : + bounds : + Valid boundary (domain) of the x values. + + Returns + ------- + x_bound : + Points that fall within the bounds at which the density is + estimated. + y_bound : + Estimated densities at points within the bounds. + """ + + def interpolate(x2: FloatArray) -> FloatArray: + # Interpolate (linearly) along the density function + # The values at points beyond (left or right) the original + # grid (x) are zero. + return np.interp(x2, x, y, left=0, right=0) + + # The boundary corrections work by: + # 1. reflecting values outside the bounds so that they fall within + # the bounds to give a correction values + # 2. adding the correction values to the original density + new_range = max(range[0], bounds[0]), min(range[1], bounds[1]) + x_bound = np.linspace(new_range[0], new_range[1], len(x)) + y_bound = ( + interpolate(x_bound) + + interpolate(2 * bounds[0] - x_bound) + + interpolate(2 * bounds[1] - x_bound) + ) + return x_bound, y_bound diff --git a/plotnine/stats/stat_sina.py b/plotnine/stats/stat_sina.py index 8c2638be4..99612f8c2 100644 --- a/plotnine/stats/stat_sina.py +++ b/plotnine/stats/stat_sina.py @@ -125,6 +125,7 @@ def setup_params(self, data): params["cut"] = 0 params["gridsize"] = None params["clip"] = (-np.inf, np.inf) + params["bounds"] = (-np.inf, np.inf) params["n"] = 512 return params diff --git a/tests/baseline_images/test_geom_density/bounds.png b/tests/baseline_images/test_geom_density/bounds.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f358c45e0d62c33c89dd100bb00c288f5a350c GIT binary patch literal 18758 zcmeIaWmJ`G*e*H&ktHEZ1yL!NiV{jlNJuEs(kUs@-5?zb2na|c9nvw8Nh6^MNJ%pZ zDe3N%IQPr?&K_s%{hc$;-eY`woF8Wm*U&MU@AE!!$8}xz{SY80BSu1agAj#6kw}O? zQ$V3`y-+BeiNDUnC!F2Crr|Fhdl5BzMJq#lCtX_ul(eqBwYinOxyh@Wjs~`NCRUd0 z%skAj_iq~8+gsc5vaneEw=XbT*&4BMoD(mDK`vN}zpz80h^`?2Q?3 zlwK#Sj5;Ss`<^y$>}eT}cMSTnDmDF3X&ub?a8;tUJX@S@TUwm1+ecEpE?iRGr2CdZ z{C!{jwrGcI#f{&W4V!B0JUsU~d+!X*j@hjxV7E5r!}wx&)_Ar)x{lka#Oq%3gV(>j z?ufyIzw4{%z2L8_!Z_zpsG@6xH&H0BMmZD?3U$B9#LVdmYJSj z{NUiguzCGLyC~ai+8x96YZ&><82R@@^s^n6?}CE2mF|2}PWpw{_l`}xPQT`*~o<8l|iW}Tog);82@jW#jF3@S^O_N0ux7}YFk)|sRa_!N2~23 zcNh z6XmGoV&diVCNUhBP9$O5`KhpQRD3*B18a$Aonqb5-bNaaN5dqMI2gl>T<0wk)8W3H<3B5LY*Vpi7S(brwXbt>l`cHu8YUg&uFBx~=Do19oiWcfGh|kSR9!oiLiy69vlkz+rh3jFy*k@6-|C_q%^n|t4 zR5{yXWI2v+Pl1l^pfkF}WhZTCwK~SMGf9nvHVNG+NGjJ6*W6e0hjC>vOxN)-iar92 zDVYKG>X#j*a^8S8|FM11;cf?Zb=1aXwbC!}J@G@nF&@l_Mb)2G7xn7hTXgrTrycns zBgs-!3I>*%Y4_r(Nl~q2gf}O|PWI|La+Q}-GG#~xB4lY4B_$n5t}Ey`x(-#nN?l=<;Jx&Ts-O<{Jc-99hJM>_hwAD=Y<-Z*Sh<1Mmj>yqdWp||JqM^Lttm$ z!D-f?F7~#thD$alhkSeDO-y8^R*g}cW%? z^xrXga;L{@ET(wn}=p-d_(w@B8B9ySV(yuz!iM{D<0uyJ9*XL*k!5|6|y^X>u=cLS(oSQ+%G7yD;o_gTqE6 z-_F5YH8N*`VBEiA%}-CvOHD8`!hYZ9H=27(9akDndp`LLeLf28xJyFgmiE`~LPRbVHcxi9(+ejNSi3Gw; zCvUhhdM^~7Kd%^bMMrrYG#32$;Vvo3*yn(?z$WoK8;aqf&}r~&iN%OKr+J06j>q*> ztGbTv?*ZmJTlnNmqD#H9BlB~~TlyI4u9+Ch#WDB1i&Q@z!8X0d$Nzi%gR0JG{y+g< zWMpJsM?8yCy68+t$lJQg2_H509c8BDeZ6Q~+lx6iQ%^@7M^rL0$YWudj{|Z4iY!h@ zz}0f!QRm;&Ovsex%aw?@@wvaVxxY~B$+$aKfr!FzetnXI(|AH9Ss*?tZ1N%U%5(UC z(~C$&FZa^LP{}{>)}rKkmaW%J)E@E{W8Ezh#o_5e)RE}=EGK87YCdhMYA&gxwKeT8 zTzU>B%v=%~vh7GJ@_; z;%AFvzo$+@8GSrb@xp>Rnt8Oq8;{g@?kADM@ebd1k5n-E6-BpXL+wX_FD$rZ{Dephzs)yi-~)kf1)V5pN>gI^%Lzv%m6 z@vtU`sgkzfiJCT(D$PIxDM7YqFjc8(srr?B1IqomlhGQqr+yCdmzUD7Ev3b>kO?{B z;LXjKV!86{=KkJUP0T#lVo^3W_8JPC(Cvte!v1c&p#FthhlhuY)8w~YmdsMAu5jE? zH)fDC^yg8e4`K95JUyfRh+7n=nuc=a z^^ylG~=)MtVV7Le`Q8mv`WmG0qF4RnIjbbOeB%d97ZJc|@2luapR2`24 zEeQ#+sMy%7kL;A2vmv)8C%s)Z8lTmy`=qu+kS^_D@gRj|@;Z&BXC>vPOGcd>T%$sr zw9JI76xcHI@W=&S#h?FisrvF2Ez_Rw8QL|Q%2^>AQH8VdX=VxnHAkxS?Wr38i;84I(g2ErKKgz@F#Ik^_n03tu#5OUtw8U zNv$esu|qAAq-frHL3(=YJd2T?!7KhD97ur}TTP*h`xAx@k z!ZPK1GqrLfBTdz-O5~*DDt2b$eRCC?GlQ6iQ?+XbjAz@Gs0CL&ji_*Ir|WU5X*{@} z*SLr*=H`W%ZTz+$b=0WE#e*V*iQvhWIL{iM_j?HU7MGW*LJ3RHtu%i z6AoU8$?_l-zS=f(5}Aa^137=CoMuT)K_7X9mf&S(@4?Gc(Z;oOw+PFb=*@hbF>;$ z>SR?2sRm2yIe&U;MbAH)+WtJt*4D>iS?wJm;nNvMK7(tXo=a@{nCrzOp>31&a&mH+ zw{F>zQ!XjYv|9F~tyC2hoV7fh>G01LO%WP(&V-M-vZ$T@(RpWC$1fX1L93kIi0+iZ zd7B}fSS1}Qpssx79*t6G5+DQu?%TS$IEzDD-!AW6kp}PbB}!DU?8%gl*F?-|+diI< zP%}J)iF-f$+qZ}0H(WF)Cw&9obC6yBtsvwzZ9h!*_sf`3=E_m#nVF%;7$ZfGTCIZv z11g~&Y~})j=(&vlqNun_r&ErJ;cYm8Yv12ge7LG-i|8|w|B5OE?XT>XQnI2 zsE+(i2adEVus=HeoBGMTh<;t9d+Ak=`=8!Tf*vOXjS7n%zsD~_TO}vekaAlbCFumlKv6) z!^nz)ZmO1Jwc^*W3Q9_;K5vM~Z!n68p3i;Nh*BC7i@ zS{H`R4l2tS%QfO2^E^|`I&b#<9o6V-J#X3Z2<&~XhV^5qP?xP$BAc~3mc6$Zv^A2@ zm?87cH*INAh_sIe41;UB( z?~RQ%)#y5I>?lv|Vwl*#!W+_Dld;DO;Zm&y&#@Z)cN>JD)y! zjo!V%uRw5alc2uNe)+Yx$I)l+=m%}8;_swZhVk><|LBxBZ(YKqr$$0%QECq9Zfz}Q z;NcmaYfDDML0qQscwHlKQV}@YnRSGtqL8OjV%Tn+D;>X_tI#M{?f7+Z&?eDw^*JD{ zpNcXe9JczgG%oa-SMKUBEo9lX{yKKy55V1jl&2!m(t@L2)}He~gQ~T?#O95o;3NGX zE*-J*B1BgQfg?l;)(5sm5e@uVd(>0q;P>j)ud2<~n>(vD>Xnt|4>rXFkN!@%NS#K0 z)zEnCi#?~s;zPdh@yYuWbuY?g)GJuUn?oeTLIxcsytJ-g=hCu%l6v{FrfFAwMjMM0 zgTPqF3lWcrRR00Mz)WUeajO^mw@x+$QEpPcgt~ET>7UCi`E#pR36u`BzArty>;Zk9n>e4_nIhnq~%Y>dcq=neIWYwO%aXxZdT0KW9l)xwLnrjcVZlu4! zDC~@%`k5^DQM>GoS|c$IlRil{+kT<%N0{zn?>*U0!Q=<`NM^qj9scy{xZ9sCpM8Uq zurEW(`wj!2#^I#Dko$(n2R{-b)wby7o#fN$h2l2NqpJ&e=Sj8EAK4i^Q72m1LeBn- zpwwoXkrYbH?EKvo?0iA_Wpb+ak#sb-+hC>`EeSSN*s&uvELFX&kc-btPUN){ucONmZCQ4xBMPnT|RTfZ^Yop*I6MectK6=VeXf!4+xbKc_Rf;MumnQ;tzC^Qo?SBE`BA@ z0}aS5DlE*%@xBzY^&^%+eeEBg93Cza12CS$P&n+W=KoYssdh|Fa$ywPn#*dNDbBGN zG3MWTPA}3$MDzM(B8=|?<14HTFYn@8TORIIi^p^M{$O3s)e#g+c5fOGvSPR&iO2#w zSg{<}JvaA#-OOAi-Mr*`awz^!nmob{elw;m;r93hl%`l_b*nFz@{imw5mC~lkBek< zJhkM5CGYP>#STXk~Csrj&}K|x`;YSNGXcsoMTZuZks zX8a~k=GYs0FPf_~mc~Cm=m8JW%E`G{YH|f~{9n_Knl)JWvD9u)A_T-`c<_9#=IbPPYNhc)~RB(^o;Sa{0r<-Y`yzz)x zjMnW#4oe*|ShLsyR)-qsyDQ9E1!gS~bXEQY^;+~`GgbXdMXDdEU|OwvdOv1Pot(`2 zxU#3nCS5p@SA>DHzYw3O$6==*-%zZWm@p*%zmGpf0fD*49^)dzIH% zUl!D!R0UpTxhP+t-VOOlu8HKD^=FmB5)&+Q)?zUPxKkr*x)ES#RUZ~RV{t5t2>kwk6mc&F~+%laD~amq8NPJcup;d_ZF z72HTAXS$K?d3t+sh+Xpd@c34F{BK*X=m&PW!v4mUnms=F3zCw*2PN6%P|{>n_xZQG z*bwV4AHN73^%C#v4^}M;3G|-*^q#2aTR(1-Y&VcL?`(B`1+i*Wz2@-4P?(#xsNeQ_ zgD#SbYffCElV0CaO-(h-DrO$`Bh6OMlS}qY%E?1Bca>sA7i#u=lzzNeyC)%AzED`r z1&NEOME}AJsaNG!+3Sv-@Gp4-MGW~nf!r(ZR>LQ$na-JE)d4kqrK9DOqq7D<sN z%*Jc4#5h*&s4&kj2hn>8+-Gd<`W`Ukviwy5pvd+(ehji?su;}+qX0d;y%kKJ)%ah*loYvGoz9*L8=k-r z=07^+b!n(WZUvFk$RrwzQ=Mn zZ3^lzpon$hqmeDBnL{@?=rv(z5P&d>QMJqF@h4l!)*W#>81-@w$u%%1T)axrTd32> zNPBNCi`)VYs}6-wny{*cOwQK!`;?D-S?E7xKED8gubYsBv&6Vte$>URcA0~yuD-mb zQ`7OI6cP&;A`7b+)!U+@5(~U@avU92sZzD7$NH63-Se^9@i*^f1>vmQIaW)ji`_Hb zFukXi1Vxu>>^Zq_>GMZ`H z`cF?{GMZ^gu|>MNt?k=ux6O?@2$jk$CtEvd)3j?YE)E$bhb8LV#)(7(XsOb zzbY4URSM)*M#Rhx?N6({>;V+br|ZUXci;7&VgwB*g@ER@5SPq#JUP))3k_w;M+5zO zp$4YYKmO`R*J<@;WyMl2MHXW+aunYH-Ykh`A?^CJX0*72#VA*oCD%-*oCEbpKxeeK zr{_yqenka0ni*ZPJYs~sBKvk>;z@N=a2s1kDQ-lXdLS`Z?L3zJjl~0@*0(5-qt1iAebOe%-Cv5Y6}T2{D=PngaE{ zs(7U1l#Uw$tkc+q?nmS2a;0K$cUD}VbDAbpZD03GZSW^uDj)ag4ZAHC`SD}U`}f4; zSFb`bi2l~Cr}FahxoV%&!0K~;h%agP_Q#{I+t}Eg?%M$T-2X##@VgPm*%r>Mzq>Dqs5 zQ9U1jiUNtj>Wt^d=v-8M@}#J++OkeGv=)(qwZ&iD=Nrjxgvy$C(U7Lz+|+_LA}nS#&LNt^|n|@w%frb$Rt&*QA}C$^Ts02p8c4#{;}ei zS5zeTXKxL=R(D#M$mi4wpFDYU-n2I}nAd(j1-|XNW5#&}f1Ow!)%g<^&`e9P(61`o zpCMIyo>U++HrDPBSIlnEL9u}h|E^jE)KOwOxSnd17Y^{PeMui298}Qo%Zl_JqJQwE zCFZ+1nI4xUZ_H3EyHRLU2tPR2(&9HFr)*W0tkue_LR>t4J7KTeV&;b@e}p;`DIwM; zRL#cW?#zdwrC1zV4EZ)_jP=AC1WV{@Z#id7QGf$2@^$Y+C9+ zjI7g~N+2OGMtisrU3YN+N@L0x!@%KEyFA(C#axX?vkC0fy!ySl$~fw!tvSgagmUU} zS}P+I*~0#A{e?9e8fE5PekAu!MoLT~zJ2>98Cmo8?K$7X28Oy*E=oULW0-gxmqlDO zi%ua9-g$r=Ru3>DAO2m(J^s;5@GEfbucp{Sq2oWad{b|nI`b2TDDA9PD_GNcFdvK1aMjK6;Z`{@Sw+!5(i21B1R%O^ZhMsBLD$x`B z5JD3yXK9&FNQ9?fJ|{(}Xuhn;lSn{E9X9GAj*lO&U%sZOq*T!w!-6Ja`65?p+OJTc zUJ6xI3S^=*qqptKFrO_)c}buq3uf!Gn3DxU$mrBR{4JO9SwT2Y7YiNO-2F)a{VN;+N5KnY>W*K|H4n{_m6?$ReJWV*yUYw9;%Bxi8A=-&OrqH z*0+?G)CNSIxn}(vQ5?HbNapj8Y5V)HNi(*XbpTt}b$lzc7>WGw;ii8>Dtx{&QjVUl z6!yQE;kvBwBG$n8SWK7BnI*a4T$~*2R@yCQw+y87^*24Pv$hthdE! zrsCHcBPf5T?)1c!gjPUv@0VTZ;qFS?&M~U~jX4(sBcq(Wym$D~R?JGBEpMXBbeNcZ zBf08)*@$H?n4|llt?tS9DSTEwK8;CF9?v5*B^8ruVLmmVBfX%Yj){p0uiJi6F$P0^ z#67dtNQUrF9><3{o~I{CA?njg7M1ynW5?e;!|kWvZEtnT6zH%#&|(C&#(vxQ zovp2mW(m*z=Y*s&j7lF&vC)ftVV0F5lFuU(Wc)u}Ay_svGBTU*N>#@g2G;^w)B@%) z>+YTL!xXb&CAn0QQtX3NL?R)c@?nQ67Bb^uuTz%Lb=v8o!94}#W9qFfUpkj7*I(lh zlai{MnwoY`)meIkynnB>w>r8r9U+_h)R&NsnRuwq^E9m|23jNfr888Rl>gZIjmXO_Wv~piFGO#(Dql z9S9^V9Iw9r1%^pN#Vyy;(t;%A%a<>6=zcp#&13s)a&oe2Z_JgSnt*1)v3h6Bb!}k> ztAp5mf1WCfA}PmtRHT+?bUmfj7x|gCn7P<`yXg&FdWl3{2Zbz|#6TL=9EFVePGLXd z4bgM2y>zds7rpdOa-O{o^V1o3-%A8f2i%Dgx~}^9l?YVDq{~Mg;{^`q(;wP77Cmy# zK&36efCFrLe?8sH3stk($|U*xIR}8``lr9ArgnOySaO`!#twH!tjMXUEr4u2#rggM za7;I*Qy_1&+R1GCN0{koRq0R|w6*l7i4g59^ol_&>?twnt>_zkF(LFIAqKC(b8D+3WQu~`i~cJsvHS537C{eOR%7}2L|2W!zq=Ip?j0-g zOqTt@OiUgah0DpoTt~=n3hoQ%L?E+f+)H}>6aW;Gr>)C-tHZ)aV<}B+mwKc{N58YA6jqATF%&>aR)=I=pnuy3>3iT^5iE?AI^TL*?99UhruB&2YID z8iK9K#M_qnC{sYSR1!8{DWhP3M z?8XgK$2%z0mnO^drGR@$UGv@P22i)PaU+B=)hlf?EUQ<5)$*yh|JjY|Fl0worHw5% zkOC)rpi$Nid*zF(cK(4{kr3W_;w~hCe)@C=1Z339+<%1qjK95w!p1rSS^Y!9-Q}Sh zoTiZi$J>f${>+U+#jdt8p_QII@%A`5FoH!B6TZCu&}!^4|5iIY5WG$(iz8w*YT}AH zSG*r6yA(QZ-SA3Cb)xsRzo^MR-gs-+9t$-~rg&-s6e<}i4U`#KSxYfG9&CJpvMH!& z9I$(A1F2wssKB8(g{HOlkPQm8&chWi4M&}#?L0#c?>$GDlFVm4;RX314>>=`)DQVC zqEG^PaQMT;20<`)Jx4MqydJALI+h{ek^f|W3K}T_&W1B_tk!ZGb)axMe+j?K3dn*# zjf=Qem3_AZR?7$^Bh=B;&LsYwjz{D4e0(Fz^qIhMp(n*%m4}B1@s}Zam=h%uo=y&z z0J+h|Q8y})2?>?}IFL?y`~qFG8@KFO>kD=r3^2!Zu`kEAdel&ijb!C(r@n!~0;Ggq z2=;}svDeP_%L({d?%~c7Al@N(T`KDEBVx7D8kUovNKvSt!hrZVEQUESda-`E^J2A- z6ORv<&^K6LdY#S141&%u2!y+RlA*v^lK5R0A$mo$%{=4_#KH#F2a+?D09+q+U8^w^ z!$GCAzY7jl?#-0ugoDSVzoy3TYKB%ce(?4ik1P`)WFcEjuee$}o3&~~O3;ZiWn4Z@UA7Z{P#ZQmM@Q0&<-Yp~7TkmWi;CXq2pUNsUPLnwus zbsjQC(c40;YEuY2jP&%Ze1WQRDEIfe*KV*IzE67n`vPp~nfF{ktN{`Pr{{?q>}W?unBh*8ahUJ=db!*$9d-Bt!IF^N*!Z9!G#XTsM{2Jg zvEp}$-Pfv8>Iu;3fzC!d4y!TEx|74ao?ckrZ+TeX2KewiSp37Y$7Zd$L3h6A4m1|_ zhaR?h@QX9%BCS9^#C2qwQEk;5O_Vz`F*&-yekk&)f;ZfRjfZ+Pb;BbGvWP? z4KBv!;Xw0+6D~^dPf8Gf50qXi}Y~Dth=6up|HetQ91-t{~`VA|G^hu|Zl@&GEFfDhyI2m#%}5 zsw^WDB5-Rd7o3)shU`7+ z4*Ke{6tqr%J|q*m0uDf{{`r+J zufw7~;*#qu2r3xD7{ybk_sOHA*Wu@Rz_|pRN8w?)eC0e9^)mC1goK3cAqu(lUfL>Z zYS|Dgg&>PopS|1U905Td3O2(NQ^>R`ynY6qy=q1-B{|3J~>nkJwLye8MaUv(A(L@{FKwz{{##y z71DRyzh8lNz}hZ|rT4NT_||by6S9R>i;Nxdk2;sHWkCEA>CKW=g#gTX2 z<~4ee;GQw&qgBe`N@#8VhzRKa1%b)Y!a!didZZc*#TdxRgjjT!!0t{8Ai+t>$Z+wA z=A@upyxV4ausWUvfCM4mz@bt(EDwHery>x?+4ezZ4|7wow=X%{4<1uE3ve{8yQ_o{ zo_u5lkCpgcwpkH>;3oC<_BQ=h@flLSu4CkuD9&~f_#@CjNsHkUHsqMOWep4vH)}v_ z^&&Md{mq*ILExQ_qoS@n0q(>FNLmIUT*~`wxm2H9w{J& za}i>wgkFY{mtS!)dI=PSDFp5Slox+hVwN3M*ik405%?|w+>oEf1rQ*l5&{`JGE;gH z5=ySV=m#2+F)>Dtk#;cC4{r!a7rRo0&p;~7lwRbbzi>w!=hjl&SE$BJw}F-p0GcJf z!w|77xz;0hWFf+}hl@-S$ zIO>@M81f|~T4cIIY;aZtr6#>iR!(?|DE^6ftJ>~TthO0qLlaL<+^BBi7e!JFxTyi1 zt6s#S&$7+L6R-&^INJU45evsij3~~11H0LFdBE=6e<&cpGjnsRA(#s?V@HVX0KHd+ z`9S`QWteLHt>H3@JUH30-} z{x8mUNa{kdfX76E_n14r$2`TkPY0w0*)t^T!Et&~(9zK?_|tl@@-@9kMMa16*v-zz znH8Qz==EZVf~|&_gMN!iSV{eit;xaWj41^Dvqvw07w6D=9(BWl-b3*Z!JGh_8M(O! z&lYs9K%;y&ZwkfWmGN@yi*ps^^)vr@Zi}#9$b#Xz*9^sYZj;q3tk3P7!elr>B1A;P z;Yz!#A^D$w;jcel8T>2^F^*BE_AD%U1*n&rX3$AR$5KzdmqT^-Ch*wxfo5{H8xf`E zL*cl_gl<(^b+Y;`>**htsCn(h;fUA~&tQEaC-(sn^3Ec` zOo@yJ3drhGFqMbNS08+^OyKSDXNx5@23DlAd|to;=gV?&n>|!`7XGZ&3C{VpC-SKx z901s*OOJAKZ*MQBGpQiQZ?eP zbcty{BPRVQkDd2|KI|CsZOfV+6-f0@5PyKDI_Fm@hK7cqM#JKAgk{n<($}ZwR z1y#WGZKxD5FflDr)e^5s$;hbu*BgI&ghNGmp3@I}pjBct_&+a7Xd< zsRHU)C{S5CzMCUMRn#s3&$Da(*23S0lB_Kkk{aLE*8%ui8Z=pcNypT@-{K zAQE++$7AvQ6j=|v;Kkj+kH+Zz8*F;)jBMlVAhdD1AKF)rT4rQsMsp9az@~rx{F&Ef zI~$^CDI~o#KGW^B@w)8ojEoG12M<=>*JW>AqM&dZHdq<0Rt1uPgM*{pW)R)gB{wrW z3xMTbLW{W?zTJOZ3<-etl#C$+zk=03=-LX+z#^?6Y0kr1^~r9VEfOy`@D{)0DsvXAOs)@?%pVQ2Svgz zuNRfGq~|{0O!yKI)lKeA$b|wVcZJHv?3xRCV=L1 z)k|}c4FwSWm2$|f6*k0VAphf8cuL;i;ug}e8^+zwg5a2~QEmy}UKC6sQy8#0pn#Ju zJ1Rq|WXnC-8+A%XqEB97l_YbFlx6t{3`JP&31ka8gwy?yN=|It(01(- zC*WT7>eZ_fhz(F$7l!F7US-yV7R2F?)UeI_VS;Fa{a#tYkJK4&d#~1S&a^FkC7@LT z8HY}S(`-NqxZvZ&wk)d0ujRrkj)(w}AoLFbfkSMP+!i>R@UQUjaHR}Ma%Gq!C}XN1 z-hT<}Fai!fXk;}DIoVAk=<0**sZhZ<>h!n0N!`}4k~6gmfC(hCM-LAY5bY4MBNqZT z)`bB^tGc*UJ!EGmzkc1skxIg-2%q)Noo9$*2BAXP-g5%fg4eg>Wx<5{5J*)*1Vnnm zTno6KfCJTp3DL{BpNTurbMAaQNcIqL2q4!7XW;@!#y*422#MP;F1PchzQaO~2vq2z zAD{m)1$nIg00~p>i_bpv(%@A%`k1$O|E_2pTFpL1KNt%n#i?(wGp;0W4sv`T{avq%vD zTN=_?A|&}5cEq(<3QqCO0!Tf&tuqYMGh{dIgT~%U1#tWDPvCZpbaahVU){d`ZVc)N zDFexyUJp}X=S~z+84+t^KmLVG9+k0L(v13{1>cwTM>v{rj^shz4IKdRcBc3Ocot{aV#*M|>WDAR1D` zXb8hOCcT+ZzBpUV`n%q%LK3-v(G6Bl`9RHBJ%OWcEn%-BPl#$y7EHbZM<(=96z7O2 zrY!@8S4L}(o~?;S7(Rm%meZJX^DJ!T;ou_r1#~qg!y<5iVtz zwGok!>1Eq!Ald(jV8ZwBlp)YP4t6K z>}P?%&&=>g@|8Q8o>nB>w*a<0+u;D2uZ8&~%>Tdc0_1&(Xmar^nIjm0y*L zAfELCpAAa#1gcH`_unaSOo(Tk-9=?tpJ$%17%5};=btA)-++4o1J?Qj9=%m{iL9%=m26)%>Mp#O9UqrV2w_lIx*Q0L<;Ih2yLm`D!cy zQD8z>aSNfffXnh1efdZM+%<$NES2+>-@YN&StS&JKL9o+yLN591>|Pb2mf-ED3BPf zYDWYULt;9Akt%z7T3t6nM zA|60}Lo^YnY;ZGMTjM`E;A_er>olK5od2sthuqeBgl)_Tyj$%BQTP7;Ru4XNwEwwX z`2W%)-263O|4DDfNUeJ{l<{vhu)>9GQb3CBy*6O%_dC-yP?-Nq+XRQ_^;i>yfl4}nhwVYu_i2Jg1_liG@8{3ksHw#Q2Y-AWd=Is`&gKOo zIf%s%T+GWyq#nmaL?~`^KYFAJAV?(io@`P+gJPz5IEX!2C&zzWADUhOda11uE%+A+ z2`8UV=Szqt6#voNaK+nN}&KApzk^*JR(9FYR0fd^M%!`&QvU4^ z!0I~!NlAJ8&Gn&O0zjs1rRGCoVWiaqDei*6i4-KEhbiz5QbUEN6SgfWFaM*Fr6$ma z7-QP#0E7XFM)dIT*P}%~p7B9QDH^C)c!@#V)fyn6tt~Cx!leA4fSrJCLBS9J^2c!M zq#4^vXo3t~M0y1ANFS#Fb&h=PG1D4_^eZ66C$x@WV{@=Q7Ht=sfkXV{NNklwvAzI@ZU21xF>Gpg0 z?=tO_L;5n`sOFIgh(t|5e@Gfw--8%?wFAGk3{oi2xf51_qjlk_W?Ikt8wpUu1K6)W zJw1I5`dylebQ_4_fn?8<%6a}7C_Ozg>`Yqeejm+y-WuqD)0YzukZA}mt zz@?xwg#7a5v|Q!9#d>eNVQ6rJB0r-9v`&8XINnjk{BC#y>i~Z{6O^FR2o56BiRk4> zWC2Z&nG265(&5(g;c>oqNPVr5OQE%y&a9oNHGG6P2~Jz5WtX{rF0yJ1Z0r0 zKq3~nbHu1x_)Ad034)Dsfx0pvm%!lQsF4*FqznzM3TgbIh~@$j?QvYU!oU4KlanH$ zr-y!t&_-BPRFu{XRk+sHRs|ImmF~I=goFy(+DUu34Q4R$lf%!j@fx5!McnDnQ4|9) zt_M1`93XNWuQ!ltw;?BBP7M7=Q%%9WKwb-=RQ=*?>g>)PA3h~}97?Ea+W6kd(X6zM zd+b~c)|mCwH~&8THc&)pA#g0m9Qj7S6oi)26|pzMJrT(G`%>z1V3 z7l^H2zkVGfB1RvS5AFAY(1!E_0q2lV5Ep+3m`J~T1ffJ}1%mV%GLI-dX+~6QTpYmP z0B>$p>Eak|#(f5hhb=0dm*&5DQwTOH{1cQ1Hsf^VJ(Cj?5)vW~o06%&N8DFNEe#ap zfkq3hqHqyK1XNIF7v%S;0NfM{p^}H)*#n*E zzdP=mE%)G@k?MDlz%G_{0tUO9NbFNQV~+`gUl}5SNOoY`WMpLDm;CS(ESc|^w1m^q zIi8;IFkMDpS5X0N0$R#s0nZ{|1z?AZho@ZU;f}7gR9k(>iZthdCIZr6sy7}1+-~jo zkJg|Phb5Jo9`uFx4^9iZh+uhA4ri$?m) z;A5oj^z7MNcwHfoViQnukv<6sFZLj~A`Bf5cAK7t73t(ukNVgrKz~`c5znzdFC`E(Z@eD&hJn zJ~3);9`pw+(u}~S*Mx|NNEZnbm_zVUSwL&Owz{%pX^|6$>r}Kb*DFx(uD`BJ!HROKv~Kk*ZFOi_7$}gojWPA~Mek JpT6|@KLEfCUcvwX literal 0 HcmV?d00001 diff --git a/tests/test_geom_density.py b/tests/test_geom_density.py index 7f0548166..f4207ffe5 100644 --- a/tests/test_geom_density.py +++ b/tests/test_geom_density.py @@ -1,8 +1,9 @@ import numpy as np import pandas as pd import pytest +import scipy.stats as stats -from plotnine import aes, geom_density, ggplot, lims +from plotnine import aes, geom_density, ggplot, lims, stat_function from plotnine.exceptions import PlotnineWarning n = 6 # Some even number greater than 2 @@ -58,3 +59,17 @@ def test_few_datapoints(): + lims(x=(0, 4)) ) assert p == "few_datapoints" + + +def test_bounds(): + rs = np.random.RandomState(123) + data = pd.DataFrame({"x": rs.uniform(size=1000)}) + + p = ( + ggplot(data, aes("x")) + + geom_density() + + geom_density(bounds=(0, 1), color="blue") + + stat_function(fun=stats.uniform.pdf, color="red") + ) + + assert p == "bounds"