From 307988296ec7590d7646b30c467bb527a148294d Mon Sep 17 00:00:00 2001 From: Andres Patrignani Date: Wed, 27 Mar 2024 14:16:45 -0500 Subject: [PATCH] Updated notebook --- ...rignani's conflicted copy 2024-03-27).html | 1354 +++++++++ .../exercises/multiple_linear_regression.html | 12 +- .../figure-html/cell-11-output-1.png | Bin 0 -> 77637 bytes .../figure-html/cell-5-output-1.png | Bin 0 -> 56110 bytes ...rignani's conflicted copy 2024-03-27).json | 2519 +++++++++++++++++ docs/search.json | 2 +- ...trignani's conflicted copy 2024-03-27).css | 10 + exercises/multiple_linear_regression.ipynb | 12 +- 8 files changed, 3896 insertions(+), 13 deletions(-) create mode 100644 docs/exercises/multiple_linear_regression (Andres Patrignani's conflicted copy 2024-03-27).html create mode 100644 docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-11-output-1.png create mode 100644 docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-5-output-1.png create mode 100644 docs/search (Andres Patrignani's conflicted copy 2024-03-27).json create mode 100644 docs/site_libs/bootstrap/bootstrap.min (Andres Patrignani's conflicted copy 2024-03-27).css diff --git a/docs/exercises/multiple_linear_regression (Andres Patrignani's conflicted copy 2024-03-27).html b/docs/exercises/multiple_linear_regression (Andres Patrignani's conflicted copy 2024-03-27).html new file mode 100644 index 0000000..d609e08 --- /dev/null +++ b/docs/exercises/multiple_linear_regression (Andres Patrignani's conflicted copy 2024-03-27).html @@ -0,0 +1,1354 @@ + + + + + + + + + + + + +PyNotes in Agriscience - 61  Multiple linear regression + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+ + +
+ + + +
+ +
+
+

61  Multiple linear regression

+
+ + + +
+ +
+
Author
+
+

Andres Patrignani

+
+
+ +
+
Published
+
+

February 19, 2024

+
+
+ + +
+ + +
+ +

Multiple linear regression analysis is a statistical technique used to model the relationship between two or more independent variables (x) and a single dependent variable (y). By fitting a linear equation to observed data, this method allows for the prediction of the dependent variable based on the values of the independent variables. It extends simple linear regression, which involves only one independent variable, to include multiple factors, providing a more comprehensive understanding of complex relationships within the data. The general formula is: y = \beta_0 \ + \beta_1 \ x_1 + ... + \beta_n \ x_n

+

An agronomic example that involves the use of multiple linear regression are allometric measurements, such as estimating corn biomass based on plant height and stem diameter.

+
+
# Import modules
+import numpy as np
+import pandas as pd
+import matplotlib.pyplot as plt
+import statsmodels.api as sm
+
+
+
# Read dataset
+df = pd.read_csv("../datasets/corn_allometric_biomass.csv")
+df.head(3)
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
height_cmstem_diam_mmdry_biomass_g
071.05.70.66
139.04.40.19
255.54.30.30
+ +
+
+
+
+
# Re-define variables for better plot semantics and shorter variable names
+x_data = df['stem_diam_mm'].values
+y_data = df['height_cm'].values
+z_data = df['dry_biomass_g'].values
+
+
+
# Plot raw data using 3D plots.
+# Great tutorial: https://jakevdp.github.io/PythonDataScienceHandbook/04.12-three-dimensional-plotting.html
+
+fig = plt.figure(figsize=(5,5))
+ax = plt.axes(projection='3d')
+#ax = fig.add_subplot(projection='3d')
+
+
+# Define axess
+ax.scatter3D(x_data, y_data, z_data, c='r');
+ax.set_xlabel('Stem diameter (mm)') 
+ax.set_ylabel('Plant height (cm)')
+ax.set_zlabel('Dry biomass (g)')
+
+ax.view_init(elev=20, azim=100)
+plt.show()
+
+# elev=None, azim=None
+# elev = elevation angle in the z plane.
+# azim = stores the azimuth angle in the x,y plane.
+
+

+
+
+
+
# Full model
+
+# Create array of intercept values
+# We can also use X = sm.add_constant(X)
+intercept = np.ones(df.shape[0])
+                    
+# Create matrix with inputs (rows represent obseravtions and columns the variables)
+X = np.column_stack((intercept, 
+                     x_data,
+                     y_data,
+                     x_data * y_data))  # interaction term
+
+# Print a few rows
+print(X[0:3,:])
+
+
[[  1.     5.7   71.   404.7 ]
+ [  1.     4.4   39.   171.6 ]
+ [  1.     4.3   55.5  238.65]]
+
+
+
+
# Run Ordinary Least Squares to fit the model
+model = sm.OLS(z_data, X)
+results = model.fit()
+print(results.summary())
+
+
                            OLS Regression Results                            
+==============================================================================
+Dep. Variable:                      y   R-squared:                       0.849
+Model:                            OLS   Adj. R-squared:                  0.836
+Method:                 Least Squares   F-statistic:                     63.71
+Date:                Wed, 27 Mar 2024   Prob (F-statistic):           4.87e-14
+Time:                        14:12:19   Log-Likelihood:                -129.26
+No. Observations:                  38   AIC:                             266.5
+Df Residuals:                      34   BIC:                             273.1
+Df Model:                           3                                         
+Covariance Type:            nonrobust                                         
+==============================================================================
+                 coef    std err          t      P>|t|      [0.025      0.975]
+------------------------------------------------------------------------------
+const         18.8097      6.022      3.124      0.004       6.572      31.048
+x1            -4.5537      1.222     -3.727      0.001      -7.037      -2.070
+x2            -0.1830      0.119     -1.541      0.133      -0.424       0.058
+x3             0.0433      0.007      6.340      0.000       0.029       0.057
+==============================================================================
+Omnibus:                        9.532   Durbin-Watson:                   2.076
+Prob(Omnibus):                  0.009   Jarque-Bera (JB):                9.232
+Skew:                           0.861   Prob(JB):                      0.00989
+Kurtosis:                       4.692   Cond. No.                     1.01e+04
+==============================================================================
+
+Notes:
+[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
+[2] The condition number is large, 1.01e+04. This might indicate that there are
+strong multicollinearity or other numerical problems.
+
+
+

height (x1) does not seem to be statistically significant. This term has a p-value > 0.05 and the range of the 95% confidence interval for its corresponding \beta coefficient includes zero.

+

The goal is to prune the full model by removing non-significant terms. After removing these terms, we need to fit the model again to update the new coefficients.

+
+
# Define prunned model
+X_prunned = np.column_stack((intercept, 
+                     x_data, 
+                     x_data * y_data))
+
+# Re-fit the model
+model_prunned = sm.OLS(z_data, X_prunned)
+results_prunned = model_prunned.fit()
+print(results_prunned.summary())
+
+
                            OLS Regression Results                            
+==============================================================================
+Dep. Variable:                      y   R-squared:                       0.838
+Model:                            OLS   Adj. R-squared:                  0.829
+Method:                 Least Squares   F-statistic:                     90.81
+Date:                Wed, 27 Mar 2024   Prob (F-statistic):           1.40e-14
+Time:                        14:13:05   Log-Likelihood:                -130.54
+No. Observations:                  38   AIC:                             267.1
+Df Residuals:                      35   BIC:                             272.0
+Df Model:                           2                                         
+Covariance Type:            nonrobust                                         
+==============================================================================
+                 coef    std err          t      P>|t|      [0.025      0.975]
+------------------------------------------------------------------------------
+const         14.0338      5.263      2.666      0.012       3.349      24.719
+x1            -5.0922      1.194     -4.266      0.000      -7.515      -2.669
+x2             0.0367      0.005      6.761      0.000       0.026       0.048
+==============================================================================
+Omnibus:                       12.121   Durbin-Watson:                   2.194
+Prob(Omnibus):                  0.002   Jarque-Bera (JB):               13.505
+Skew:                           0.997   Prob(JB):                      0.00117
+Kurtosis:                       5.135   Cond. No.                     8.80e+03
+==============================================================================
+
+Notes:
+[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
+[2] The condition number is large, 8.8e+03. This might indicate that there are
+strong multicollinearity or other numerical problems.
+
+
+

The prunned model has: - r-squared remains similar - one less parameter - higher F-Statistic 91 vs 63 - AIC remains similar (the lower the better)

+
+
# Access parameter/coefficient values
+print(results_prunned.params)
+
+
[14.03378102 -5.09215874  0.03671944]
+
+
+
+
# Create surface grid
+
+# Xgrid is grid of stem diameter
+x_vec = np.linspace(x_data.min(), x_data.max(), 21)
+
+# Ygrid is grid of plant height
+y_vec = np.linspace(y_data.min(), y_data.max(), 21)
+
+# We generate a 2D grid
+X_grid, Y_grid = np.meshgrid(x_vec, y_vec)
+
+# Create intercept grid
+intercept = np.ones(X_grid.shape)
+
+# Get parameter values
+pars = results_prunned.params
+
+# Z is the elevation of this 2D grid
+Z_grid = intercept*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]
+
+

Alternatively you can use the .predict() method of the fitted object. This option would required flattening the arrays to make predictions:

+
X_pred = np.column_stack((intercept.flatten(), X_grid.flatten(), X_grid.flatten() * Y_grid.flatten()) )
+Z_grid = model_prunned.predict(params=results_prunned.params, exog=X_pred)
+Z_grid = np.reshape(Z_grid, X_grid.shape) # Reset shape to match 
+
+
# Plot points with predicted model (which is a surface)
+
+# Create figure and axes
+fig = plt.figure(figsize=(5,5))
+ax = plt.axes(projection='3d')
+
+ax.scatter3D(x_data, y_data, z_data, c='r', s=80);
+surf = ax.plot_wireframe(X_grid, Y_grid, Z_grid, color='black')
+#surf = ax.plot_surface(Xgrid, Ygrid, Zgrid, cmap='green', rstride=1, cstride=1)
+
+ax.set_xlabel('Stem diameter (mm)')
+ax.set_ylabel('Plant height x stem diameter')
+ax.set_zlabel('Dry biomass (g)')
+ax.view_init(20, 130)
+fig.tight_layout()
+ax.set_box_aspect(aspect=None, zoom=0.8) # Zoom out to see zlabel
+ax.set_zlim([0,100])
+plt.show()
+
+
+

+
+
+
+
# We can now create a lambda function to help use convert other observations
+corn_biomass_fn = lambda stem,height: pars[0] + stem*pars[1] + stem*height*pars[2]
+
+
+
# Test the lambda function using stem = 11 mm and height = 120 cm
+biomass = corn_biomass_fn(11, 120)
+print(round(biomass,2), 'g')
+
+
6.49 g
+
+
+ + + +
+ + +
+ + + + + \ No newline at end of file diff --git a/docs/exercises/multiple_linear_regression.html b/docs/exercises/multiple_linear_regression.html index d609e08..c53bae2 100644 --- a/docs/exercises/multiple_linear_regression.html +++ b/docs/exercises/multiple_linear_regression.html @@ -1020,7 +1020,7 @@

61  +
# Create surface grid
 
 # Xgrid is grid of stem diameter
@@ -1033,19 +1033,19 @@ 

61  X_grid, Y_grid = np.meshgrid(x_vec, y_vec) # Create intercept grid -intercept = np.ones(X_grid.shape) +intercept_grid = np.ones(X_grid.shape) # Get parameter values pars = results_prunned.params # Z is the elevation of this 2D grid -Z_grid = intercept*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]

+Z_grid = intercept_grid*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]

Alternatively you can use the .predict() method of the fitted object. This option would required flattening the arrays to make predictions:

X_pred = np.column_stack((intercept.flatten(), X_grid.flatten(), X_grid.flatten() * Y_grid.flatten()) )
 Z_grid = model_prunned.predict(params=results_prunned.params, exog=X_pred)
 Z_grid = np.reshape(Z_grid, X_grid.shape) # Reset shape to match 
-
+
# Plot points with predicted model (which is a surface)
 
 # Create figure and axes
@@ -1069,11 +1069,11 @@ 

61 

-
+
# We can now create a lambda function to help use convert other observations
 corn_biomass_fn = lambda stem,height: pars[0] + stem*pars[1] + stem*height*pars[2]
-
+
# Test the lambda function using stem = 11 mm and height = 120 cm
 biomass = corn_biomass_fn(11, 120)
 print(round(biomass,2), 'g')
diff --git a/docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-11-output-1.png b/docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-11-output-1.png new file mode 100644 index 0000000000000000000000000000000000000000..de5bf531be92a340ac1c089fdb91ddf2fd1db7e0 GIT binary patch literal 77637 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 LW0Z#-r?34V9S+gb literal 0 HcmV?d00001 diff --git a/docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-5-output-1.png b/docs/exercises/multiple_linear_regression_files (Andres Patrignani's conflicted copy 2024-03-27)/figure-html/cell-5-output-1.png new file mode 100644 index 0000000000000000000000000000000000000000..15c042e735fd49cb79fc6648d06285f2ed2418f3 GIT binary patch literal 56110 zcmeFZg;!PU_cyxf1_?JPB_*JwAfR+f2(m?_yG0tLyF)1vlny}=k&=?`5)c&u2}u#@ z2B|yO_xBz5eaE=t{sr&5$MKwF?Y-7|o@dTa%^i)or$k14nHYsak*O#vXrfS9$?)R^ zAwK-d_$vZD_|GkOMP2s?PS)<8X0BE!bu)Kodnb4MN9I>NtX$n5IXMdPiSY^XT(Na` zcXqqY&+qX6{sBHGR~!EB-?|UrO^BS8_1sV>3Nz#f)?4W}k5H(e=_(4cT3#8ejh^lX zd#C5;4La?<8i>ka*!V6-i9 za|9(}ei#)YJ4?Sy!@+d5<(f6FSAV42hfrR=S{ zv))Yjp;)BtUKcO0@=c%JlJ52n)XRbG6(WmHuDp=7WL7iLTET;o$;AXMdB-n_ox25O zz?*nw(FP=SXVl;XO*uy1dn0dcZQX*ss8I`lGmk(IGbl$Ffz@A;^XO;$;zb_w5Cwr( zYPJpzuqz|jgI&*z)N(Qc-#q$hkuH}@*yoyrr+Vl2XI7FvJ^cO(Q#yPZTUv~u=cQm> z8RBrc?JYl|EPpcnd;MpnF3+FU)s6=59Y5ma`U|%V9#qKm z_IM#5jy<&VW!e4kWM{}_SdmuI&&Ac`OM7soB5U=)=s8ZSU6ow%h+L6`|4}Saa=mZ7 zJ#}S9hcT_=ccnMb;#;|FL{CIb5gmMit z8uc)|yu8YH?@~`j1QRCnQ$DH5(M<`L%SEYwV@gThX>!HSRufCaTjU9{(lcvC%~0MY z6iuW`uGFhN>CC~7md5&O=r@S{P)8^7<456zr6u16a+6DbgXa{&p&F}#dO6l26*+2d zVsX@S*b9R~Ut`zZ<3@1%-Dy9_82QmC%I0FCy0TFkvaY|b-t3$-6)-h5MY;X{OjIWW zm))xdkMUL4rUXU$0Dj!OVMXd-hdEu5+r5MEGkXrLb;%SfGXXlkuIwI-BJSVQ#`9xi zbW~JS7nqs(C9W#I&d9#Lf2<{qb7`X|?1@!)-q(Cl^+Z}PG_ z`?*xEvBZ$V36WT)xuZzB%i*S3hVObEpZUU2i0j!@`h~{{3%2Ck%UGLHV1yYfU>G8u~%~? znKx6PaLAJPOSP+h`{Opo{M$mW^sm?Fhp?sblqwg#SNdz1C>9UrM8AZ5((=xL_dx7=^%_n$J^y(#VEvL8<~3-6M0(CC(ELz`%ecWaDx z`>|^*~xsENy1Z-tKNrNy$LIN|t6(m|X7WA@}~vQipYQ z?)*-wm^%xqM>EmM{ddtcuM*^9X!TOUN9^`|D34=vof!y9v}5=8J$Up#5gjaLm)-M3<&5|W}aB)qBYs(!u^@TR_YAePuX#64-UX=@n77QDJmOg_HI zLG79mOY3iCcNCjDw&o!$B9fPvhuz!To5MvctS+C6Q9H`xC~3&Vi?GsTkPcv|tEY*Dl^<@H8{m`?2W%uQG=4^z<}9@md;s8N*WF5F#B9?@m^cqd*8-{D#RS{n@)& zUmu@@TQmcQ1elH^@CnwYGM$73W9W92{GB)<|O4Hzw3!1a& zgM^@_BqW|H#-gLA1*u$;{vt(@DX&xDX@b$gxJ@=zP$U-|5Z8cvwHxmKl&fJe`J$oUor8kJANJWoSz3x(sZ(Y z_q|E;^qbCw6FN#)r=ceS&jwj>P%_Bq11$%32t4;a4G8K_*^vBLcrg|cD(uG+*5!m$bF3K7jp5Ac-);rwB zxUBay!+IQRT)l`$SJ<1Xwz*sq>xrJ);3Z#ZC_Me<_V%Rn z^YZ~h``GHto@bYes--LXS5_6!$pWoA680kLf2QJ_(R=HH$?NNmwpIG?jq1@)o;q4c?5CF4?{-aY4Mdg7p3%ahw)2DH9_qYYyOuCGP>l%7& zu93v3VKyb=6B8-Rcw%>V-AmaN8mHE7)i+$_gr_Iu3bw= zN%>vD`&Ge$lp{&eXZ9-9C~ovJh9$nbJL z@5N0kgid2v9sc(xlpItDHO;d-p;eDZ=Nfhf+mAPz593-d<4g+Z)mOv%Vg2?@g#&uFU=Wlt13< zg_6;WGzp`yZQYhSe&r>3UweVoKg^9mDP*8Ehm za{MW*vWn+;uzU0s70SW*R|D3Y68s(6873A2%kzUnBP)Ay3^(ayy?2L|?rUjkQkcp<;?DYXC2?qS5J!Cj1N4)xpnK7*GfsvX6A;U(r#^IW8>jL^ZB!bUxCaH z4h~%0+^v9AFq)d!^;2w zQ8G3*oDmTbWiIn~%(9b{Y2F*wQ4x?cY+mC!KG^6^cWR1un5Z`EPvJGT@TjV)@|p=I za6a7|oiBQm)fR$B;&==|S$d<($zKj#v{VC4wyfeLee$YO;eVPiAsvsi)azyKgYX zBemJO6&Y?87gR{+Wo}$;zfN1WpE$+&|=r2Wd z@`Wqf4S$AiE~-1zMYJv~uz&sbEio&L&%0Uq0xxvjO*JJFt4~?6g^$=dIoYmXCn+y4 z*R8hKYw3m+=}+bF-Uv7lN40z{xO*Sa*;1A-`KXbn(U;O^gqQ2E_ER7JVW`~lp3E^x ze2Nlund&~ox64iNp)Sx|-lO-wInIrJ%6rP2!M>SD!v;4JB|{9TNly4p9B=h#ed; zI00z8s>|Nq-ducqDZ5Mn?^vf}XbW_?)+c;@ec|T)rpXlCFviuGLg|WdDzOKY$#Bf_ z>gvW9%TrR&$;rtmXWTz-Zf@nC8^*FS?zb7q8E=PWpDb{~M-mqwtW8AvZZy)O!sZ^; zPw`p4!IF9Oi2H0iwRvVPmVe({O%GFu;x=Zt&KC+63HrY5ZtLiH=gE_s_yh!}gYwbB z!!+{I+Osi*uOoym$`cX5r;BeiP(S|hz7=_xe#aBPx^ztZ!<$dnoDL^`o#D4^)zr?% ziEnPUQ&>PflKbf#5Fk};KR&lJ)-mv8NZ56)_-(S>O zZm<>St0x@W2Sk58MM^dBD`vmV;9r+%s4N2)5|t7%GM+{<`49pER#sC}u(IOF@?F2> z?%|PFRdrF^b6vsSo+pk`{P)gFSP*tN7tfwOd;9JkRU=)2K9|H$ep3?@?7WvZAE%TJ zm<;C&CF=jlcM*)fn%C=^V8Qj~s7tgd*vW66-nixgO*ryF6q36bnQo3>m0Mdb@pBkd zRD66l?4Pc#uCUyotIvCv1g%Io2$j!{W@d6mV~yX1e;pl-gg?t;+@Px|Bb{c!wIGqm znih6FX@f)mHj0{cc028FPq?#;)GrdgTGa9WTKmqBbo}hB87Vn=o2kG|q7qZ<;i1^O zckfzuBxs27ZU_naGd-xrV29E0C*l2=$V{do>9o9&TTp;EGBUz{nD^aEzV;S_P z3R76ze?8-ZY#lt|;h#TavQJlSyyl`g6JEWFhO!I2a<06asXc;>-g$FI*3f-eQSMy8 zsV9UAHRwWZj>f<7ts^rojyyRf#b@_;n^vA>!YfM&N)@0o%u_{u>8Mkt)y+a>Ds@2` zxsQ1LY_iaE-)paYq@fAXEb5@vkO)q9{6utq{Oerc)~#r0Gxp;ZczSwzsGx_%+GZo~ z9-hTtT&Jbyt)=BpBEj&qlWaQV0%3~n#IDkR784T_wEqN6<4SuUgSjF+;g%K8^5aIx zl^<_dYxzn$ukdwFu<>?rKHElr87ySHL&_zlK4H!FBnG(YzUkp3RZ z-nhR*#Z{4`9L6&?_+oRxMbJ<05&%gc$CcIt+kg3Xs`-iHmfdfEtg6a=_l}sGn>*>Z z&Ed*}g|m9MNS(@Y(&CXASdjSrc}-lEKDRcOdqUB+uu-@jyl#JLK$`sOsy!eyrlO)E zV}n*e%#FtJSy@cUsi|x%EUil}RGM8Zs~0wzf{f6`}tGGUyDk0SG%)ZyGe=M09EMI_)B zJv=<-j+U~!>o=NWpV5ovRa8*HwF9T1bWAz^_pc19jKtpe2Zv_|&G9BsM*`xnlf*Dj zuBa1g-o~$YvY^A~yq6)PqC(_;JCnU8WlSWrHR$lr3$6ti2Ek4>_MP{GT{_1uMVlwf zi8Z9{Gd+oHT)e#D`X&1pt~rUd|F>uoH`G zcM3ceVYaG?62A7e9H}kCkDSHlN)F_`tS2R*`ck^M08vIFGBh zoLtVrf=%n%ML<2yCx=@(F|+^XMnp1i5~eYq%PVIA5!_m%$Gna7hQ z$HKBLX1p;2PM$SsuXUBp<0+$>Z%+o6!dh9S>Kx`gx(w*^?BvMn>}aV~fK=*uRS1ZP zT>kJCK0b2jhwY$!BK)po*BT{j6%6^3Pen0iXqb zsiU)#g^%x^mm|9v{a?&$eRZDxyyj+Rcpqy25A`DpP9pfPI@i@-HXY89pysq62$XY{ z*Du?;DfY@9ZOWN{;OUnu(wYN-q#M>ydU3ulWDYOIYWT~Sprj-!vn>y$&n1e^)i+4- zaV}rJTxQuzmCGoX(+SW!x4Ie)JB`otOOPQqv)yD#p=tTV8X~on`|R1FHtF3e^h-b^ zGR~HFu1@RGh?_@*p0P3AUh|7jM`%u^3{i-iI@wbwt491;3 zK1S;K(nHLNl||zFcTmA;XlY4qO`Yvuq)vxYFlH~JrKOcqSt-=xiI%W=mviGXrbtUx zgPL8;K;7Itf5Z+@EdOWI1FM^LHRB0?2icbAuRd3yqgu?6Vp0YA`^u4z-zjWPC;_jN zj7l`oHSwqt14RWE8zi4@`&Ai3s&c_(EXQ+lj93_P&U(4#JD!aDo+aw zuhP)aT#=HR-YEC$Oq=ZNo3gu+K0rrDCleSbt($UDN?x$I*TAfVedGbo79HY!R2R+v;C+;{Pku*iA-Z^yt&m!^zpokcuw@#fVnLoo-Qly3&wj4o$-PDdY1 z`%*YHY~gEaYL-1&(KZY?T9l)T(z<%}s?7alfiaCY-*W5g>4ARpIr?z-gt!s|vv##% zQ^a&sSMtRE`{#q&{PRt5Gu8F83j+`yK|#S_z&M}$qVpSXz5cw*Vd%NDuKhqwjg&5e z=+H4;SAF`EbW}9;%mtc*(++d>V9Vvi4~_Zx`91ypBz=1YTGzq_n+MhI-J9#AHR*U< z+IZE{W^Jq-2RV%Z!8rD&SSJI|Pm#^=p>baym|Q=9GXr!R?^9cHDVy+TRHU8UY(=Q_O){AYfx=2wJqX?K>S?`98M{1sSP6&01Ss>ig0 zk_U#(rKNx{j*gC^o;~aQJ5sy_O^&Ivvs27xmuuBST*PsbOTu$~vJRkPS1=9%HdXSd zNg$)>v=;pH^d`wLr{J7rXCLhBfUN|i#-paQeVPa&{Z!}cwm-a>H9ge$RiYaPFdc+ zFa4+HetNRvJoRK$_RTlh(ovH3-IKNRuXp)#p-Y5@hI(zb61c3_E#-aqKrSyY4=`U@ zUH!>8CnqNX{4~jg?NXYe{dTMydXG$Pe>n= zLDGf2_K&2Nn7l}on~UoLUz$uTEtVanAXm4ApTeDh}Y{-8`m zTUR&~@p1P1cdgH_gb08K)^4_um{pGFjMy2yPlmQW{4!P)a23MCCINl}$&ocPW8tfv z!3G*$+VqbYu9W!C&B3ujPl+K5KR*VZexJsd*e>2TMMXmkA8Bc86HgT2PG^?um%J@0 zQPk6WH9E?E)pZy>k-(cg6Lq!5@^$!%{RJ9mu0T2$78lLNE3BYsj@5gXmNf#zmXwr~ zv%%;7BK!jf1-KD2X^%h10<3qeoyA88;PUda^1XXq4mDE|BgHxdGzkPK&;a4& z8S2&pYk7Ky&9PytE6C*FX(dK^i?v)Wq<4<>i!fPiXUL~dw^M7Qcg{48jHjlizO3x& zmYHJF5G!KJQ*C9)`!t;L1{N0oARi*-Q^9xC2@cxo#C{`Tpez(IdMDd zyoOCA+Oos3Q8+j_JwrpZWYsE{lJUgW3H1#O%DuMCK{#!eYzyk)j{xa5#@)=~byNzT z*v~=g_x+ET1~Z|R1dZ4!nwT(qtWPc{F{0_|W8Y*4T&AX`7P$Nj&cBY#7Ih2D75?Uv z8@~rLNwSv8ACK}ehAMiz_V_wJuRT;}HH4TFP(owP!r720zAME|pm&Q)9boj{|=A|=HHT`&kr z-f+H>3v3iD=P0rxzCLseHPGD9Dhzx8vc(b;XDfN@r0=$ai35d2z9yq4adho2rsw2I z+F|730)H$>IC513sO1@hTUOw42w@FkT9r>U*Mlqkv5J1II1nnq= z--JAWI10-4=WJ=_pxuqdrx$3PKpMrKG!KzOZ=IZ+aNmr#q{FwayaZ=)n@f|mbhz>2 z)8lG5Pau*FXNB7zjP&;yR7xH!)!HO8iaB2~bpc~6#+OAOZjeJC?cPMa7N# zvu1So=-AF;+vtIT3)D<=ZIu&iQoL`J)px|xqpHJ=J-ihofD@enJoohG;peA_m)MTl z4AjHS)N(n9q>t8L;6~#U&Edpd`wp7Td%em#y5BQu?n(wIDMb#|qYuQQ$Ev%!?jU82 zh=@qaq+N@pjfR>UCtQw&og6phJ9(Lc7s$t5-Q9}WoQf}B>)s7I=*k2)r~ff5#V3-O zDMti%LHh$GAIRn0+#LKHl#ntwB)#1+`m!z*ZLvne!5x^-)^TyqaWpe?JiJ9&O!Sy{=#-8ffo|r#NO)TJ`=1d$=a*$YubEg2 z>15ycLy5WnxnP^N|1-eQ?rCp&Cr!zRdRx6ez20pOYd`I)bN*sjvDyW^mJ3ohS4_?? zLpz%BW&!~lAT270*Pz;T^n;Np-JP#=D{n8-n5Z)oXzy(Ur!gpl{!3 zuY+Ft^5vh(J9rmN5|lZI$181=gdN5vR-)Ml+k`KAWp$sQ9Z2&SRFkR}e0{TMSuD<~-VVpk=Fr>OQG7VGcI3ZAGIdvxJ#n(d=^TGv#B#u{b+Ms@tQOt3^`1Pl!x zpwAoWQ|SUKuNv3S#Ou1?Z+*AhOXei~=R1^mML(?Hx)Q79A9i6Ga=oS7w&AdRUz1i&4&fG4Y1H4Wt0Jrh^Nqs374y; zASE^R^D}(e^k?X{>#uu0{#I62W|o!;ACj~26O!!Un8(2Y6F?-OnJxGGp&(UMRI~_?{`>b2 z)$@caX_J$Sv|XljG#_YqbaXVaqlbO89bPTsKE832FJ*h+UOsE^(K6ObQ(_ULPNSm)AVvT#DP7vM$pwf4 z?3@OTCRU&7Y0`YG3-nB1Uq38mB=IA95O?@wx-Gg=KpoFqFq%ly-(NB*DGAl0__9Oc zbE?2mWrp%hFv79#=59anKoVuTDND;NTu$BLRxB}aw zC|b&4kWE^xE6r?e2|?>QT=#5_H94811@EYKF6R2?Jo7mnioKgRUmNp!=J9``pPFe%tNqGqh4 zhO)B3?05U>Dr=hEU6wisz^*RW{W38T3(ud{j977$U`ZhpVUcOduHYb_?3D)Y(Rn;s z%JiITJeY2$cWMX*A=`fH)5TPifLjO&?(H5N9PE0bW6XVW9w)xu@}tBccXTE68s*F3 zkU57$21Z6j4GoIlpI^;a`d3QJvQUrw_+Yf+BCEpt&fdw1U})#1r0@Q+KR0pAZ)=PM zh25E-X^iJ<&FE^n7<(eT$nSVB*`;vZJkLgSOme}y0G)~NrrKFYuO!>hk>}FJJ2yF; zp&c=oIR%Un^NBzDK}nJ*9qHlpK~v;Xb|4dl^hw#~At=?xep`wSfO*BvPj;|S;vTCl z;2MBF)|z9Tr^|yzT{yFbb`JFjE86TkaZGZuzMyQGE)P7<&Xc2^T+Jfv7&kG*76fVp zhSTsN$@u65BbE9$$V2@3`?n2DbzJHfcD5~lvrlkUGBZ)Ahkg=o>({aC{lzbIiI>>M z<|@!9#)v9x8v@Z#Zt=PnMf3o;Eu3@E{)`lQtSh}Q@-9hvPj_@V*31L)f!GK<9j~Ls zRG_a<;i)50hBeIkU`fq%%w4`ZTzXMQcgo~^rT8|-8jJa^XKfa4OM}SjlQW22(Zhg3 z586zqQN4$mmlqvrdr%%1DBe$>J)57U)O2*zBYBuBZm@oVPpa^K+K9oQW+ zAGWKS$2^h}FVfRrzjoEqWZTfm6cCwRwzU!NnDegHpR&F8YcUab44Imhal3cUMjinuE7ozBq2gzTtYy6ezJ8!pgrJOxcU)b=#^WJY+dzk)zZ68kkEY z{0%|!k>sFq-v~UfA6mM5 zYVi3Fx$j_zfQU#0NV14bOn3)h(?$-UGwAzWd5W=yF@qiZcpbEvU}6F#0d;6}y+>U= zZ;L8sDfiDRZX3hf_%88T+bdrKQ*Eh}oe360qjMP~eP{qrzy0t5J!y-m$`3Ro@c_PL zWH7MYS^2!VS@yc<*@9$hN(w9Jjy-qMuK~e|x&jOXcKf~h)C;xYa)taU$;nqFC8stB zOPj!aIjG0F(Zs=?Ccim-=Y9xd|co33^R)H)rs^_$LN-Ow!n0AD>TO_P$-( zum2T3*GkZu4#}vR`!x17lXE+b_b%3H2KcZcsR0c!5_$&_157V25PIEO=iGh^08o$+ zxWLpr8$n;@Fd_2#%^T2TFV#$W&})|(Gh!lica5d?k2*b zAo61Vtx(++V{B)Z-)ai+Bk;0bbxUccD`At1jr8*%Yv%q7AnRmrGKWrI}AJDuPokUIiC?Q-L<-g3EY%A;Gt-dBP`yrV^=gll< zmKWH&^WZW%bK56lFAwZN2fzlSqXX26I7lJ1I@XZL9zm=mHQj>yKS$Q`MytJu1$))4i0}P71nIzXlrQA${wI3_TiL>$2XkJQBPF@iDqafuTH?L5v*z57UHw{Gwq2N34&?jB?{{%uW<6kqFf_f6Ew27wn-4i_dnkbLC#Kp7}!v6w9hiqKY6xZLaw zH@5HA4Gi+HniMR2T*2!o%+E;}+DB~k{j~|u6r7<1lUTI((%~iU|j`!`4xntz;*>mnjtTkFh^mx}py@>?|Iko$!y#3&eUx#9w7NIGW%%rvcE`ZPX1zp|40y3U6%NPf_} z6LctT-5?DGsD;{GzOiGI3z2~TW^~>hh= z=Z7z5$shR-v^!U(nHCi#s%WllU4fE*tJl=eQt~0I!@ux*Z&-=B z6AZ2Ic2yV-Cji~>j{sd*PJ#Xi7XlM?jo6eMv0YNl(~X*q_&SA%R^Bp z8<_Ai3>3QQ7oFH?j@9h~otSTkD3V0usxt^^rfNE}ELsmA5;Z>;>(lxXcZm%F>mag2 z+KKDhHS8#u;^&OvQX-G(2*uFz1>TBO~KMSQT^NKX79>Gq&_QNYbI~ z9=2H*p(^YoK<_DhMt{F{YNH+JRD%`f6d8^$T50&8y32(2pX(*Mpl0 zBNa0-Uyx`vvj6g2ls4;sh$LtWeldw-=k0YUJo%4x4S54{L=S&Di@hx>Y6a9s1TGpV zg4Tq%6bx}1HZ_wmv`{}LM^e*5?S5#JF9r7Ey zOG^#yye%zNva`EJiXS@aA6Z&YUiggue!wc7sc?w+hQ#JlCP7vn$R&x%$?TtFqHt|k zb=Ozh(uvKd%pN~}ocQ7e2jPt)n{wTT7<8+@alPmkOJF)w%KMMGlNJ7MP&=SiYw@f; zS$iiQ_ydeJhn&~plJxm9tKVx8tIn8l=kQ8;nn9v z;5q76SVmuXPS{~&EqobCKYjlEIjS2w9tJQ$TXRuXFt8on&d8O0%iORTcNK9>i{<&lx_kPvJN7jU|a=QVE@DCo3llP}Zr>i8KW zk~-?%zwL@1M(_@3ie4ahBZ^`0ycjI1A>=L&kuZ|;d;WKL;ooh%+D#r`tQfEr27ao) zi+y0lo8WLelN@wx(8$AM^_GW>?o=mUPC#Fc3Zde8y#8X6|q4CG?^W9P*X*Z~gGr#BcUs3?3MyJkVS*azLd}N|}04 zSwQvo%8DGeCFL?!MmT7RGo_fMzu?uYU2pX7mSfa^w%mjS*?c*I1 z*){n}ZB+xG-D%sZs~hFHPlK0M$T8(W9Yvy{ie_?|O?+Pudfo2sE}@=Z7!>oR^NBT& zj;=0zhg?ISF{_;5GdJF;3ZRoN9Q?j&+M@z!>6~YcFcIy67os zX*vCV4{95O0|sH2x*%iE@5IE*i;JbV@@tkpe*8$t{H__jJFO7>+4loPU2e9YMkfwf z%lrC@Yrof>6|LmaBx$slRFlpt*cj0ooL_AtvBWwD1*8m2Fz_$2FO=$S?*}fWaU3k2 z91R6_O*i@iq#|dMhF@O^;AW&>$AZEbB6zK2XRLZABm z+ZyG3>C<9UX{m+Ay?c2dKYnk2h#nB6kwKR%s{(~YWeL)hrxnPs#O_1)KwJ~Qo%CNF zP;QXC3AA4DtdwsLA~9%a+kkxG_U0f;)B!~O{(}cNRr+h?die(jhiuF2)@>FRhu_E?RJ>1<^!z!3X|KaSLSFUn_+41t_%NDTIwjfVX-xQI{!gc=Z9LIkSH--l-lZ!fy4-E2N zzcr&ZItxz*;2#?Y2ZyPSUTtDYi-I>jSxWQ!G6{mzK0v#b%=26_q^ z%rKN_ub3CW_1slu^bBaedGluN%7=RAnHDTS3A20CuKm1-r-)Piy-+8$UMQ&2G(oQC zXlLoJe!2q*~R}Gku;UGde2oy_6Y;pYW#Q6L5is z!W;EVf0gF?X`ew<&{QGc8EdxcliavZ_`Fzdih6;j*~WBZTQzJCIApc%R-4@*BqH7w zB+LC^?I4+Y2U1FBn~~Bp1*yhu`~h1esNmD7{GW1(*pyGmzep8-Pu<7_9Q$`hi!CmA1C%XHs-rH06tuC6zrYoPg!pRP^R%tHVe z2@D}59pG8R?$^6xpPpO~YQgpFA|WpNY*gczIr`(;84rynnTe(*t5&7B6Yai+Iv$)l z4D9n6B_-KntsEXS26cz8iOw*fNq}4|LCOSnu6FT=m9AE8A$6Z%^0JaJ-)`bxVbZW) zi#IdVFhp!zu$=F64hVVwWwSVw>jisCx!t|Lq`pB1-Gc&QZk#8e(VZ zBcfX7Q$oq^cnj>7*CR1{92|00d{_LbEal~KzJC2G;501(P$j-ex#=gSR)cd_#3}TI zEz{b(vv}{!(stg^hbcuok47rfC=rcwJK0!ytP%V3uM9bV^@bRXmeR#p(IRQodIQHgoE zE(W3;cqHf+Q-QZF%g<*sC9#5A2cHi?Xuz=>*Eu~5;Te7Sz{mBXFDI==JzSYchihoIq`(_k zE)YV9lCK|BMd_)U&&^AnM1MAwcAg39UvWWSd7;YDYqfM@c%UetkDgJ^=A`;|Ie3|q zYhkAZDhAk{NU#G0rig_xp*t8PTt`ojq+4jIg)yl*;6Fe`N#W7MZaHUN;ZufS7>u4E zBFDmaQyZXWkZuDpXl5^CmNiMTy^w^x=t{!N8DYBGuPC$weRAC^9LPcD(-BcN_z{Pp?<4=EBHy>}1fcP#L2IcWK@<5Kn^&I81b=w61{ z?D4b6=*j`PThEtVKDb?p^)5SeH(8zm&;nss;&9ft?#@NMWZK0KFLXIuPb+?vBeEgw zw5z(;KSuwk(Y2jS)D*HQhy5mJAxIYk9#jhf92Xe&umChCYgBxnpDW1XA1ewWzZUd> zImuY*6q12)8cnhX>vF-Kqhn;eXy~ZO>9yjJtFB5`_3|%Q1xpFB<6Xrtlz|Ix5Ag+->RJ zf1oI|zzB$gSgiag8LM^4AG$5+nP-dv#}YB7Za)4IHO;L|1Uow(%+F1UjT;l0_Nf)k z0DhrXAY%W^=m*0Mef072Z~a)@>94H?Uq?ppU`21a z{Z_t5!1+XFaN?vOl$xX%wkq-f71|arKFvXF`|b}$^~>ehO~5#K~qmM7^XDLk#?m8DLXw2Tcc(D%lUUNQk># z+E}yT-2Y?Xmi2}c0FD48!20H~Q@*amet!z7U>1JV+jP9% zu1ISX2BM!CV?9uet!dwl*=M5DG7tu;cASbq!geNpTj2{ka7$34av?8|n50PZ4)KSe zHzVmb0h>VviR0DE!||a2d=!#5-Z*#(hn_eb8ze_5lC2XWmU~mZO z$zoocIlo?Uv3q>E)C}nOXmcJIEn2VN=xXvm{xUqwvRtK9m4mu) z5zFR;NEOs;V342-VvgMyB7wBPP-G8V4@c+X$nY>03`e^L*sO3QnNv9ou;i=MvhLNX zr;bI`xNph~3Id>SZI%6;F3O+d?i~2a6Gr_458IhdYbKDu2onAJFSP!b!?)hL|j^tm^zk21VaZ)HtQCs>b@r zZbkCTA~{!>Vj2n&Hp!Fu{ko&OJ9f}#|52_0X-fri>kwU({B)Kyuas0-PQIwdChCxp zwZuYF2os`z$qxv68{BIAgfEjJyBCaami6zye#HZThWu6#F2M0gmu^PgdJkSSpCMt( zaDjY@wl68p=ic7dFJBUVStJdKz-SKyFcr16Q!vljes5jTPN1=F$nm1a1*rhiq>wew zFD~w1IcridgXw9JbagGYhRpQa&ZrnArr>{HSnRQLel=WD85s$BUZD7%Hc5l2xBeSc z2+(b36w;m5Z#s+_-1fS22fx~3BI3W3UPu9>+P&r2xTlRwI-Q@L$}+bz0>0L|hCC=_ zkkz2to=DGbXIC{r3MUHLoQSgpnB3LXHN=kT5jfprV^PSsU?5L?Z>&E?ulCEBh_y?_ZhbQ;}NOy8CQP(d@&PxeC;Y z4di*fA*3}qxZ-E2kRbG!a6sm>wwB*y6F)E{dR^uN3<}8%pu}$50@*MH4w4k7F?|4u z)R;!!gSn);e6q&IOyF+;=LEvL1(F3l4B22c?!QITaMzN=4BIMz3iPhH7##1g25%?Y zY*E>-5cjKbWc)cX0iEe90Xd2oZv#g-d2vt4}G(RyQbfh&uZ3 zwzEzjOvU-Z)ZSdxsBt^ErO#k;j)e~ah)`C6z|Ta`JGQ{;BGJZ$1xw^!bQ^uDUI#U} zc^H6t0vEUi(Fu}X0vbgGLV;E%FDol6ll|=}EzOc*rdB&G8V%pvffHmC^I`04kmNZG zXTqf;UH*kCUQAxYJwNszWVb}J(}-<_gH-W2C}~{n-+Z^(zRhYh$z{M5OzF@@R}>yl zSl@t1WvCXImoa4eRc=9mj6tDVV737(t|BkBD!vXgvC8ewH6P3I+3&p2Vf{U)G05_1}gB4S(iOr-BGA&?$3{C8h68XMdc~rYh zuYim_&&ywD=mvwt)JjZZj82t4V+#7>APg!&Rx8P&R%%zeb2ZUPBp|a zVV*(2t^LR*XN|kT|LToz&uyuak-W_FoP>kpHWGdqqM&waKCM{Nlc*e0KRr<5M8~wX(eULvVav|BU3K;E^-JzR$l&fT=D^K{B|;;? z>(QTQt_++&AdjO)Jk^d@v7%T;80sw!3l;bxbn=hv?Ajo6bbazmQnux(KC~k!{XcRl zlx@OtqKtCb_mL^V@^Zm!T9+dOCoEDNv2LAYRg9!0-SLV^@JMkZdG3}~E_}@c3K;=W z?CcqBV(12+)&gAdw&Q5r;H=2qyGgg@Jb}M}=gD`p+hqMo{Je799zq&Y>4%gqaQl#k zyfEphd9Gfw>?^a&yF2X4R6KIKz~$2{eA7u@ZSB~~J}{m?d;fCY*IZ)ZG6y~cL-unS zZiB67r^k)fE?6?=8Wb~4GuLTmP#qx07wg>RGiJXai!(F4FyL)cJ`WRbAaKDj9(=t< zr2mJ|=27auZng8JQ+3kH21H8;jiv^A4&NZcoG>wH;2OF^>bYGH*`RJ02rdV_p~>P^ zXqRIy+5!DFJ6~v!2FwW%<>XT6>#UdH07s+^^-kYxg^?*C*jYmdm&o&q$ng@IWbpr1vqA6&`HBP> zGUgMDb9>~r&bxBYKBc~NoW8@KK+IA2wdE9Eebk-UMVXgHg2@ETewgo6oTIG$6}k;x zm`V>2*0rs=9JhPY&hVG!8KsXON4b=(8A;DKNDTb-huqmj~_pG}M@8q$j2 zT2OsXpNM+jOmdu6lxNWP{7O_hm!lZzbd$R0@qb z33$+RQ(wIB#_cvtc={m<{dj_Z%>IKEfk&hvAA-sAOrJ)e&kh2dt+L(6ME|CT+8 zrzI)*#+~=9gyI26anP9TxL=+r?>E;w^WsGf5#jqcxVbJsT>EjmCZfcBPn;4Ju0-{I=MJc;G`a$Awj)H|3ZPqapoSa2{YtR*v0AALDk;)l~-q zCw$QANz;=xxf7>7JzK$|I;10xAQULzIL+Nd3iL2$BXCx z`^aN~Nb)*B!9`i@T@?o#FAP+0Yg$?H3@qM=s2#5M-+q@{R zF8+7Z(?d_6HX>;;l)1liyBCAgMS1IUDc@4>d?PeBIL)AxTy(p3=e(F`6`4mRJ>V*l z3Ae;&PpDV%TRmAUE0`6R?|stMQ}Uz+SWuYIRT;v$(d1vZkQv^^WrQ-F z5UhXwlJ%dlf{8p%vn@;V(mnA%i;E7pF&KK?nLacpYMA>#n<&*gGbcT1hPz%M*GRnE zsEDlpjdhmXZi#5{jH~^@`L4DWYAN6IJq#2ts<)R_^iwa~)yP&=V+k6)l3o2G+OYt{ zfKOssltB`YM-vfo*q`8Xao-0)3;suUL}KlU%L3#ybb}IJrbZSYSRIODdI#Yec>+SK z2YCi9E^WL7h^J@ldhR#@$Q4%Of1$3MxR~CMJTcM`0rpVz_F9`{u%ps$znxirN(3a> z>0;2~Xb9`tebWg_x{&Ij4_Ur@K7EeQxXzyJC+0AgdpLvC|B+I2@1l?3Lo-^a367&sI}yIOC*px7jmWU!QqlA*=b%@3wFze|?2k(|c!gXue147ARN0MhWWR za4%o<`{eVLr&pnQL&kz^(FPN+oAi@6FpR!`mTI6yUGoZ78je^geaON@%vUjRc8EiSHsjMP}Jj>^!~R`hB>8uEX49I zM$=jpqx8x)c}U~^7c`IH0so+~b;rWv$L2Uk4U_x}9(lbi(U%$i?- zpP#cl^MG@7_VOv^;OENmSFY&k=tK@?*Y>Fp-v8=bT66*vT(*=-nv6kaSA{vRrjT-T z!+E&FM{GA)ZJw@$2K()u>a}G`aMGUm8{SH8R6^70Svu&$wk&Gg@MA3M{luZJM>M>f zhYd+VkGj9#7D433kj_zI_a7}E_|%T{Szxi_HJs|NJ`r#*@5AuB99>j4zJ^^K3=rDP zq`AxVLXbB@%#%PCm@$M*&q^yJ9SdhHBuQz~Y+~ZATb{O|6Doxf7A$Sa^lR~aAN)4g zOE*TihP=JC$%f_^>_djg`ey33n@ZT%k23DqN&Rt7R=k+=WBV4nissPi=R*q%wRHxz zW7lu1{O@vrmWzulH?cy{n~F4ad}aHF&} zC9Ns;hFATPsLwh|c#Pr=cQ>T&xEd+XXKiDU#M_8IqS(JMbl6DYXiqZA)c#?esaw0i zeMSFU@InW)aiTy)v@mLqO%!>1BzeP8Q-EBcwTIt3`hk0$<4@J>;HOX2aLN)Ptv7Bv zrjJ$9OKTms=40Q<)>o-~>DKt)aLcVTuO(yFN0J0C{d!cW^>f@a?XKxJn?aj}>nvhh(^{^V)v;?#g!ty58+} z);8Psy3L(>Iro1I7I$91p{K_?pmQabDT9irYH?sfXDKcfII#ZLm+{Faq@fqp)kIr6 z&3mmdmL}s%;lKJ#non-jcfc&Ut7!gxc}^nzZJ|u4aHnyS4Np#_XnvAy zvQu0jguRxRX8J+e#bO@D@&z~L`rw4*efn=9;JG@G-(aBVLC8kQE&NA89!cPXf|HOj z(PHA(y*-1&(=cL7q&G{hcM$FS)}HMx=UY8_58YLdpP%>2AzhW~+gSBYjW|VEJq{RC zz%w#_vV!r^`q~jwD~6dS`Us@k>;EKcl83y0(E6lE*N2r=Ya2s*(AxR&3b)^10zX}- z2x31|U@nob%ow<~C^-5?Cg+)jo%$H>IMm4YH&M_VUY(8v0`Apmx(P~M*VB}8{{2)& zg;Z&G8Pi+IMNM>n|86tgx4_`$->>+?JC4QVvEt_+itC-i^sgCWq<;NRLSGcuy8cD| z3G6=XdR2c`g}939ZEUf!(4mul`r~#2or{-ON<~0fTj;Mw?KV%cQeGP9d&+4>F_n$l z2QtrCN56E{`=YV5WB9}2MsHrNBXRDoI`74@vSE<1GcsmtnZR-mP5&2|-a0&B8~^Yu zGKS#0Tl0qFnCqeSp88<|Mq{W@i8rr ze)ss6k1Ts$-@`DKN3BPeRINs3LSea^I_P3{TrwBOBbS7jgE20r#)TDQ1+B&eb-Mt- ziTEE|OUkO!RN-h-+741uq8Hv)+$Hbr$zA_Di95J1w7=X%3CjMT9fm-G|k%qi*1_c!EqlCHG53n;cV;1jqHwbs4v%L75u<0+WrJ;cs;aE$GUr(6&Cd zCcTL~TG&`7)v%D=;XKYM)sUi3@v?ZKs5(ZZRmf{&-^t2*5iWrOoye{G3DMK>Dhi`D zEq(q+=I^n2Xj6z25WtTY^%XL;pWR}ipP=fdP%Tm1MSCf9!@CIOb++3%iYY&|{7cUd zGX6X@IKOIHd(Twa$|UdoHL3vqf4d5IncHeIX}?%@@F=Mv|B<6R1mhV)6n7E@D;rBt zmxjf!;~o>=h7M0$m z<_&~##NiiK)onRr8|}iO-r$j)6TJ6z!y&RsmI2xPlsk7ME)EBNO@7wg7D+W!_m8#w zg{nncTceEC7FCtclRRCJyYt zxeG03t!1j#z{f_)J2+pP^Jw3_f?Z&_37tmRa}l%&O*goBst@+Ke~YX0&S%lD(t2PP z$Y=50#FU5^KDSKul!)uVhy7`8aJr-q*_3azJz*OZ*lMQcf0S;H8w_zuQ<6$0Pc(&g zD4NzT-nnnf-9@3W^};LGzR+detP(Mi+y{+|Ls7Ede7P7yS9g7~4wiGVojpUBlFo3$ zh#;VaFZoxbv`}8MFhoq-!{c4a4ys>I=pXE$(s{PUNW{uU=iXZe2mLJx7TL+U6V9kB zcI~3ozu!H%F+iDv+ce}Hr1@pnT#Ce!e z8{1mKU%#TQug7)opbmKxAIGiNT-J|ba(i5f06igzwZ-oUWD}aNthny(Rh!a_Z~^=T z&n$~Y1cj2RRP~jvTC;ZgY10}wwm*#^^aBLHx16U3f??g+v&vlwNZ7MywLydOQMK0U z5LQVdT3%p3Qh_=v%UyG~cis3K44*(sKvK1HaL@_(b|U5W}&cM^lP#E(qjrth-y-1hXs_Nxkdu&pzkMT<=7mA5RlK*=r;o{2hYB|`Ck0g= z1J~!_X0(sBowX6LzZ=*3F%~Z3nzQ zSLFqRdV#9FyK1KK#niX>+!!G)#n`*s#Kc8rQZq9NGZ1OxLu9}8VdCiExuUa}`mxDi z$S3Fqhve}s{sCsI!7MD2l9IZJk6;}~uXD$;)SeRg8hDk?M85L!%Ma6&XTJ>Fm`P}# zEH+h82+vgO72L@#deG;)k#VE5leVG)<>GXOz1odCQ^B1n<{bv_bmBR?>3lqM&+;gk z(D2F*RJaS=+qZ4i+-uK`!?_{k12JJCmPE1%0v*M{c*i3?UtLBw7C>UVsowDhUP8;*Qz_Z&KUl#iT^YmAx0EWJs# zRF2^)JDseP6M##AP+EYp-FNAE{>#Ij=6W{;L75rv!3QmBq6|DA_9 z*xR|R@Y!Gx1vuhjzpYUkwe8u#&fZCgfE27kV_|iVt4!%$k0$R+BG_5`)Nh<$A#`CP zQ;AHQ|36^sm|m&qv|eUmANd7;>O#I@+OH=(yu8te@|wRKsHtUgC=ymYA6*Kp>p6w33HIT)xO2?zc6It4 z^ip?v-dA+ICA~w-Ua<0ww6{uDmyeTJ4Q=wmEKV)l^0^#PUWszp3hnQl46fMjD)iY= z(y~*yHDt^iG5MC39uDWYirkTddNFcKEO^5%9?NtTgpyxIIF>T0Ws%KWy%A} zzclc!5&E`s-DCjkcZn`llfaTgqfG&&`_l-^O6EnPDUy~%zA;zf0<0bS7E%_aTFiXVNq(`IP`XZX60XGLvG3or3kr7S zF9{q%wF*Iwh~~f!uFyGDVwZeR9HC_VzRCNfh6aB)LB<8_)5_YcvtvOWlFNVmvjZh& z1^7Ln5Jc;BYizy#O`~d)J#_P@QqGGJy5UWQE{e~eO>0&kHG92!Mdsb_o8Q%;r=v|q zr{vCfu6GbV94I!G77m>UeYYvc*U&Ki^eNK1jQrc9W75|SW&ZAIdkp@iJ#Vlcv zY+gV9kYkUIDH7GDrlv#rQe2H$G4in0l5QEy%=FJWp>0h8jtZ7?$@H92!>ObPd* zU!}r4nTiq)u#zYRRi7oX>P?XnUAJ`Ol&AN|3?QZdF{IfU)zDrSe$G0wVy|@Fjy4~z?J!Q4 zE&3tASiO9`p!>+%wP|;j!2w2oQtLwL<}i~><%+Y*@sh=7(Je*76T~0TIj&c* z2}xSC=6LmzJ=Sq7vmlJ+AW48&9l-HWi>lE&x_#xqk<*nU290!d{qMX*G~w0ctb@+6h9x8lf25EdoNo^w{M5rFyTgWy`S+#0qvg* zl_Bz?l`T#;d(rMD&`tJ;AQ31e0*DC(LvZq(s+=%otH1yyAqgPfOPqd$lL*n5)gLTu zRPSvb)YS0eo1H0+Un>Vg7=H@qjKkc=)B0WqZ#u=OhI=^dO>ea})fZ-;t)CUYss1-6 z$Azc;ZM1_M!?w1hh}wChS7U;n@X^JWH4ZS3;)vGd=(xzZCyq_rRzLcT_77VD^~ z+*Ii#y06yO)<`}veDUDb$`%YZ)@W9LdUllj_*f zQStX0KlpzhHETG0F#J&o4X2dyd3)|FrooHb#P74Ls(7b3%aZhk#cKz;O>`c3<-W~( zNG>c)GEpO6Bx5zwu3G(7NTkz7LbDF-@`DqTAY*=OCMi0DJ=C4tiyWW{GCq^n9I+T=0|Js3U z%CBLaF-wX;^Nw2r+&R1cnA88EH+&!3U(D}s-0o1;fE+#aQ_u`yl5FzR$-baE2G|Wh zjzz3$dl^;7lbpB^T0C<}9UM!bHQO;U0Ll}w(5xOsR8D@&8l@j87rtP9VCKcq;>dvK zJU))Xvg!=tHk5N-__>Zjg zeU=)_t*ei_soo-GoW?n2MTM0`k&~dU{9OF1J$SKj^pLFdq5PUNs3mNViLO}T zJ3}ZU;|iiev;{2+17N@2jExPPmNU(0*C{CM&UBEovNoJ5 zPU?$mB_BBLb@>K$IA8=cOhOOxXr$UAUUyPw-TcX|q2Vv>l1SxpmMrT!W%)hww|5ln z5)-skd|htN*M0w)awOrp%=Ri-*eNLOJ2O-w3lsmyTy@9^I9D>#F?+D3v+68#bfq_!E{XA?m7x_*7@pWmZH^mGjn;%}kz|EzU{=6_ttlTV#v z)3F*RSPK*78tx6QYtZztyo!uIi~LQOsFqG8&tKIOKgZ>Qej?N9*u|M_!8J0JF}(Z84q*zbLyXJM6fSvC^;JL4%M<(cWz7)P zfF50Ecg{*Vp8QYZJxM_QIiPJEpJ-xj@Yj~t+Gj< zc|ghddydxgTyN9QPmw#BaT<_mdA<2-he0psnIiYgRPC#@cQ18%$Ib~26|Nj=OC|~u zRA{1gG{3MAlE4}-u;N-eE}57#s*E->;!iMPG~Mqp6zA%|?)?474=Ufxh4}a(W21&M z&XKCeE;Z#nj0Ad$kB!2Von288U()0A=XWn#1y4O7FFM|4(q9u8CwIF+%4*!KX1m)9 ze{1W^uJk7^dYtSTb%pO-6vihfn~{G~uM^*}%q1mNx4wOQFxjr?dZ;xK83dK!Oj!w)w>qjJ391`=5AYrLkyOD!^Nt#wTbg(dw8x8PDV7qn=U-^ z_xBf;mhNnR&I0}{k$FRjBz(2q4x~cmmfKr-g{VaHWcOP5CL>XxwOo_U#iub&@e(*14BxGeue50&419cwgEL zZT=GUMb;x%pIm6?9)#04fT*PbqdI6Puz0+CrZD_nZ(83<9_mFX2iUpbv|zsfL2!6T z8*H$-M6Reuk7MO2iQhxd-(Lx)u4ZYg+*mQ0cITTBIPT&R-7js6!d({nTbXOmqtd%H z9>Jmbt@*9_bu!6S1QYjiVlsv zCA)Eh>PJCtWZ&nfIj(0T1ov_ls53pvYfF{LxK8=_kwi9wp6%2??RHoyG52K84KyD~ zjM{U4Vw1+~s6ojZ&gG;k$sx=M13qc!_YWVY7MiOKdH_@^?CweRpo#huTe{^Dkq9OL8mpj_~i?zCJBsP|L;nOKoKR?y? zlY#s<<7qGFK5RN&k;N%p$g%6>NyE&HmIiFid$-@Z%y96r&DPNjmF%_EY+jzL*MCh< z-+A(Ny6@>3CYYf6Q^GEsp+LnF+`r3%_Y#8j?-Zt=GTl^4OT#Z`Yn#=z5X1(Qz+S+D zG4tq!`S7*Rb2&wEOM7)AN0spj>7MfB10`IuDq&X2MW78!R@n!lY@y=t~nJ$(ycB2_Vzh#X+5}k*~!MO zjz)YqdBm(p5I#R_>u5X4GO*3NiJU zXQgZQbWR@w+aQ>Fy8i?HBvQ`EpDpLTsynS8ES|paVlytJwY7Z5jvdQWaP}1xx285G{FSP;TaEQS5J~T;>)NK3w z_A&>1(kRgp#`!5EDylcnZ5J67#1Zy%ICF{66k+$lZQzZe<=COesilYTB#_S@k$;zv zPNPO-d3H6>k2$&FKuq-@nDpFcymL-a7TSm&T9vE=|uDy5nUMLO4U=t&T zO-M?fd)*pU$GQz?6uc*Z=Ge*K&i*i=Ue5NfY9jdLf$uNxfz%A-uzh_uerm!cP*n71 z^Lr}GJCL)ENf_|OMR(nDb-y$+xEVY0My7H5mAIqgDOxYcu1#vD9A7gMt=zg@Ykh2A zHqZ8m@2BT3YA|o7M~FVUH@9~ZEt}dy6kDmP{byf_rdi1gpL)!UBdl^zkOy(3=g*1C zwCUcA`{k0SH@XK0XXUa6P87#K?Nd-v!qvF>0d>^dB(a+ZL^!V?rHgCN#etYdJ*66d z*{^qL(6k24Mm+i4kQpR>3&gsN_AQPjrBduNwUn(i0$M7v;em2;aRtRxK^=;AGFAJ% z5^riDy+k0vOYO7`i^$#M8N!7_(khMF*fcbLJ3QwZP!(LG+hFBN>~N-N^Tdh3Abyj5 zd<-mXy1Qc`GBvzoTcS5-!;7#gqJ* z4w>&6vC2O_297VIDd)(XZ~ms|ioB9c6Y@t~R)Ny|c%=0c%9?B9j9a$~LenqO%Of6- zd+0)XDc`1>9ePW9T3(1tN%_!TSbemI><~R;nJfT<>R$?%6LZkUbqomTyT6=@0Q|IH z{l=;(>R7s3n^}!h;u?H!k%?aU+{JrEAj%^IW@y@0wb=&t?erlBXjwme&<4)+ciz4T zNeS}$fZyzcF=DtnLx5$Pv=%3L-4I6ZIglT$J>LBJSnvDy7XibTU zW7C1QF5R)6Y(lyOxk~hMC_mc7C|oKkDOnoN{Z->GLjD`TPIyL%c5HoLfw|_G*IMMp zym7db9Dc8x3_t5d6Y4&#k|ad_u49mow?5{gu~V^;*zs5>lLrK zEm{6HJ#Pn_5e+gHe^gYFaPXa4>#x+Y9Be__(BbQDW8T{9p)| zCuCo20{X%(G2{vNgz`_FYW(yu+_K&#m^t$^!`)HR4Z7n=%@%{JpJ-jJeydwjrBnAubeHbi+}!r!yH|&7iYt?=Z(;zLcOV1 z*GOGa$;mWSq|s5+{;+*+o6DDscl1cIv1Ok-M;1%AFRaedgO%jLd;Rdft*v33^lWd0 zIJMqi5+J{Fr4(Mg*fYcXVvh-R`M^ddN1|38q4h>0^W+NYv&I6s{WyWcJT?9I@#$^1VKjMdehc{3K4C4i2&Fo69uIBMwkn z;A`VE(JeL;{P~f*Jugp<{F(afh-OYQU)0Euwg-~oR&iY&8oRYB27WDh&rVLF(W$dP zfu4q%nwg7>E7cpq077BLs$3O*yT`v+nr_`X8nq^J1)041d*i`_9#;ThQ?s&Wy`^h!?SRw0@w9y4ltB@j)~cKP z(gGFhpPmUHFeGK+KaIkwER~u(7F`qK*`K};Se-~2u6>R6PjQ9Nf&1Fuy6+F2<+0MI z_K>hUgn6y|uJKI3vVtB_=ti(M_i~N8fd|qdbBUaw_;BwOOa1XpsKuEg>-NNOVjcwi z>VCyn;Sc$|_Z-UbP#NRL4w#Awhbzj8(74JXEYaZ5ujVh_&w@~4WrAMZ8KK9m=x)GIzt zgdH@ayauG$-`fBT(t0K&?pBNg97bs1;f9Sfzsk+=%1Vn{)CcOM8hPKG zD%-)5IXrBS+qO=6N_Z?-2tOJ~?9>!Rr2JHtd|y=y`8`L{Z_~~+kd@eexx5i7QYK{q z%U{8D*Mx^TriI#9lB0?r$Erv(zT(;X@wCpp#v$=0cKYS4i?cH~>5Gof9XOD^G<#{j zAP>Dt)eUqqT8EoU2ZQ%imW1&UHBey>%PXPn+qYA^c``ixz?Uxb5XSG!_FUwODk-8w z-ZZ;cja5m64or7o=PHFqCA7NdVk{QS=lhGXOK+cLH9qaJIicCPg3zZv;6S)<6T{C4 zur2|*K?URSy*)Lhw&H0#d>8C`E7KPyO-v1gZVI|4z;7zcM(u+lonS2V)%olik0ck*x=81V7l4CK_S~oN)rrGLPM+N5 z%17(yK01;upKj01Pi;(@KANJg9JdieFZZ!KBHFvFb?UHY#7a9$TmN&*t=_(he~fQx zc|JN{^=_o*1y90d2Gjojq>tuzsGnJy`lc?NIC!}%w|h)~*BC7rpPy}UNpLK*Sw#L2o~6$G;p z4^eT?%E;Ico<1Icq=n4)EL_v@|IV7CQI^U9?BwlT4YjFH%(n|>1{`A6zVzCis^&B| zpvg9ml#$ex(ra|L_C2}nBL@AVTab0*hsWWqOi`I?T%UVw=xh5K7hd?jJKsm@fjoI| z=dJmLoZT`s_j{T&Reue?Z?~!Abpn<9R!N;#W&g-Xwrv^tYM%xB^YTbzV_2rOrzS2l zh+~S0-#BxAW#ya&gB1Ph8yUWS+PBW!k`i{fvl+?`K?ixKGfIM`k!WPy7OiT z7qy|^31xSZ-|WB@_mIg`L@M9QYyRY(MJ21OS2*Mll8p5J?=1nuba`5T>YH38;?qBV z{D3geJYx};l@@nEWJa_4#S6?46qY}oz%a(T`+1!E(+sinc$aig`K6G;U@ zTvj9u*3FTSe!0wLI|LQq^IOE~N#>Zn5_o@Lx|cObj@*-OFD)HgR>UwqOGDFep{(Jt z=@xHAuy|vKfDIU z?#K#);(((Z*@dHqt1TVe+Jc`Vl$>_9UZT9pq5q|j5*hZlgFAN@_K#&Shq+glSxB*@ zCm9x9L)2oIulHZ&3pV_>&VMCcdsQ|B3>ACKS0R>D?ZuohtLQXh53mS^g24B z$b1P>v(Uni4tya;`9$2G;bv2e@5#T($t}MpwdOQ!LX)q7A^9(lEly!!VJ@?pyXdBp zLn&R|7!>0Df2&vC69V%-{Cz?~_KVr8BGm5NeB=B3ai|+x@hV@V-CNBU7aswGR`3{K zhPrRBVNr%%k+A#CQvLf89nOh{Ls?EIVHb)KNfL`eYzVxL*#3USo+I`DcaT8Z+}N8jUwVG?J88(+*dA^JZ2@HxjKl5i z4dWRq+_ST7|BmY3A#YWQ*JjYO;YZ*m?oB(h1H(!Dde6V6+E`sKTIgE3^T{Oi>RVyk ztvg%yp8;S14i1}BvywMnr<`O%)e9d`yrDlCuK397oaMWg=aB?pxn0*?jiaiah>Z=+ zZ{k)*`}y-{f|K^C#8yU~UqE1=Xd3&;sP-M02M%}85lhPiC>uo+pHxRlM9IOgBMvhVFky~Dac?DLA#+O3FvmFky+z9LpcmGS>k_zM>VhqNKH$39# z53ot^-o3SJNGyHpqr5EY%ldPoqOzxoOIzE7d+bP?z4cfd-p>?KWmJUL%F4;GgUZIh z$awKtKhw5dyN2pEe243&FEQPR;O=&T=kcT0N1pH7OYP_;#g2)!%8b`vQ6gC-$g%fU z{e6pzN-O;iWy%k^oX?;)+BB-tdH+IGS;DtON?ZnmKY7MnMVWz3g({d!U<+m%sSs<) z>&NR9e*gK8;nhABeV>W+`3ajqDzx@1!EgGF9KV;*?BS`4_!h00R^a^&aKFmw@~?BC zXK(la=1D`E(Ef*0+3ZP8{?V9o$j#rIK9>8;`8G&sfHbNm?4V0!>!Ajj)*H$)tGQm z@@6@L(AIlUe(a>|fP;Q#@lpr}tpqzAo-&5q`ee-cpFiv*yUo)-=30NX|0sTGXG8Zb zCnnnlLXZ(pVNw7tNs5k=VpElCGe$!t#-u*ssFxu%PRs=Bw3C(%< zuBFLuLe(emEteSiA_=%5qarSNn6lk$CyPOMho03Y)N{XvW{zaxK_l`4l>&rIB70_z z)y~)&&Q~|uG51R!FPNFS2Xl)4@#CD>9Bb%4T%Td@an;HpS#aX7p%e00Ur&lnJbPBl zhQGlxdv!H?byUFKx~A`UA3OS%l3g?Gl>oc+BB1(P`46jNyU??`eNsHPk- zF{NEuOcy$M(khRL4=XdGSFZWBisq}`Ro=lW!&0QH(OQT1$tLJy71K`oT!YL7Z;TTJ z_HR@GtrP-b1+kKNzUYZ8is?$Tn}7lJ>BXi!*X&JRH=N2Ae>qhS}cRot=k>O||ESHkibCwWR$M?4urb z1L$%Y(*fKlT@`SLBJFVF*z9y@Sx|?_?A&#C_pasHLW9g8{Y;BybFUBqWse4T9{=o{ z`-aE6@8j|JmD=lyYf6)d2@Ah*s~vgTrMd8xHly-0tS#iM>`w;ZDAZW)a1V`wH4juV z1~6WzY{J%kYw%EUp$?cTYm4OX*Z~XuYYlKqh>3DPGnLY~3Qn+`>(F^0t$UJXR!V>7 zf(opR<>loco^KN>>G@nT)1hQu=df+5tMitNQ1}zCiiHJts^??SO;t0cg<>u@Ow?JY8Zx3gk7+Mn zqb&stJ4XZgiRmg}1g3!IyRy3Iyi%RmiRnxU0QeJM1g4G#@G6Op60YYMl}5=iHWJ8B zaNGxi7fgtB-_-<7L_A45^1nO*SjC|+l4|+Y;m}BYN4Ia4EaAc|XTio;ULH-3@w#M} z0;|QyK9VLEle=^Y#+zZ~q8`Is)-B0wPX)C0*HwlKn<#_HbME|0bLXP!y!u0(cds;i zdglL{h(Md+(;uIFfNji7w!)EFdDM3wT@)D|rVYMzcEOylA!U{oNHKgcp0k|&44*bJ z1pwSkcTUg_ zzy{9A&7}m*jtkug{C6O1b3I$?CT7iXl+-Tr41}(oCk9eO(I*jjR200(C5#f#4QTn8 zZ^=MxF3`JMR{u^?_#Hs16KFAl$f@l0J3(37aR|xMz zV4zBD$g`td-hrP1_f=AI#In+Pm#P)Z7$bS@O(x_x>`>9ui~}0$Flce896M&H_T!}y zNh?#G_OG?SpmP9sFnT^u2~pVU;;x(E9ZuO436C9LbNEV&jR4c)$;4zSd59TAIyN>o z0rKC!L1D!Hlw@HABs3F_)O!)Drn*+8!$rsao$(bh!A$$)jXJ5YrhfmyD-a=zPE|0X zGw*qT;rB7Kq5tTsB=r`Twj{pFjnzqA4!(>dv z?~w2$>=<-cOC)@Ek=e{+dJD{d`(gIyj=gPGp2|*Wc(-qFOD$3y6SPdVgP#zMUcnNs zdEYvZrT1TIgOsFe=5&(lPn7n??Guzx?U^|vky5@&EG1&P2gnC-BG}_u1OW9#TtSHO zygl8an!PX=aLFvAHJmPr-_U#A<3SqkhUxH5$O9~;zYNLHCLPte<6j3<>kl&lZ#^d z49(dkB{D^4Uv*N-@0Vmx`Etz=q8}8T<^eIcrJ4T9j;`z76R)#2|DD0zcob057#N-b zpPD1)y}-{UQ!_`dx%tlU=3Aa!KM%SHBb{DuiVyp1V2pK`c_&$~x86 z1myW{%;sYqV+^fV#mGCrq$3*Gq&X83HL}x zS?+9e3DEwD8&~QEpX(d(6hX_Qtw7)cNa+wyji?@sKMa#5R;YPh&z;Lt__c)b){6YW zbJ}Tb0E9tOp==l?@2?9{gY|*nqMM}e9lm|$?rpOm-Y3$;Y~JVCQ&`pI4YgUlcY4Pu5$w+jT~85kg7qL^!?+kZKO#W;jx`zHlWO+uB%TmB-qj(niC-Igyl zC`MKA7QU7}ZcDg5v%I>B-xl8tan3E1fke@ZBcyjMTXPq@(Esm6wlR zowGp;n3(H`!g>_gV4sx@1aT_kzMI#5-D70B#bNqb@D$@G4GWo#b?T8)hety2VP~ue}=xuEWuZOgUKl|8=izZrv#DHn( z-h`*zfovj6_q62XytsIAUJ>8a-+%40Fx8%;{e17Qxq%9HR5i&Q-9Gfq``jN9UrzFI zrNisCBN8|6-S~Iy+O>e+tz5r>`5eZOU7DdSon7flOn^5tJnCm&IrRycYuM7UDhQAR zv2j>=y%Jsi(p$&*KD`;!1oq3KY$0Op7;SX5L$FncEF6&%EV=mAocKwhy?Z~68uKvz z&-BdviL;*)B?Qbkc(6N!Mjmv#0BJ9L)M86pJ0>b!FZFfy%llvBkH7b&`7ha;D`n3? zR=age3#0XL%3RjJf2sAmw7wagRlk5R{RaRx1MX(cXV#b#P|3_pzT<^~NEH)DKHwiArlD@YkE5mQTH!3Q5E<_mK}YUe2{tIH_h4i<#<1Aa7z_wYWP_qbr{ zV0!|_G74ZOpu9!_l+!(StmpdRR|p_uRHenmThYqp-))vlhZ|?E!A*uX&q#L!sXpA# zV=?)lY0Xtv_iUiW1;PP#7ZI)qc37`d!H;F>;*Q zyrj-OTWpP4eoT19LlT~P&q~{F6%F}2BXmXNjUn7|ASUi_R= z#xIv4)F08XuMik5oR<6ocNT8hlrde8>i|qfqplRK6O5O}zd+iSVTn4(!PboRqi@*V z63$&X+E6xvhYsn#wD%Kh&J&UL?8h)88MLUPg25!lWS1WS!ND~KmwG(_)@3Wx(J#<_ zrqf-WXrbGdEMNe{Fn9yT!UrID3Lc+v5NT!gI_ze5axWpLg~%U+4k7ig4o){>7lWu9 zu>Kc7{b$CJlbeW<5Zl{-{I;3^IZ$(nA|2O3fLa9{`(t|y)klqG6fYsQ@?NKaC-@?3mY%&;TTF@r1=4lyCyB76MYLu%Tw&<@9 z3p2Im&aJ$EMPGPvra!TbKS-)Cxm^;PHUSdEW!7UjhO5c>sWwIgS?mIRQrq|N&fW!# zNQj?B#zp5~;uaGi`xJOg40kVJA?6-$K9$8YKb%PQ6Vt>obZ-9=B<=5tVs&>6&>|!t zBp8-)&)=zYw^{ImY9rcTS7kEbU_})KN#Y;fd-G8}?8CTXv%Jyg!JJUb;+x;jxjst4 zS5c^>SQ`S|(*P4r@rd=P4jjBZ<<}c1GeDUK3p;mpY#;b79;T`LaVoN7!F`@J8L(N_qpo`@Qu@O`~#I;*0>etUBQi;nm zF%^5crzDhP?pzȶ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 b+x!;#ZRZ=Eu-$i&g0KBL2er$owh{jaEhEw* literal 0 HcmV?d00001 diff --git a/docs/search (Andres Patrignani's conflicted copy 2024-03-27).json b/docs/search (Andres Patrignani's conflicted copy 2024-03-27).json new file mode 100644 index 0000000..cfb2af4 --- /dev/null +++ b/docs/search (Andres Patrignani's conflicted copy 2024-03-27).json @@ -0,0 +1,2519 @@ +[ + { + "objectID": "index.html#preface", + "href": "index.html#preface", + "title": "PyNotes in Agriscience", + "section": "Preface", + "text": "Preface\nWelcome to a journey at the intersection of programming, data science, and agronomy. This book presents hands-on coding exercises designed to address common tasks in crop and soil sciences.\nCoding is an essential component of modern scientific research that enables more creative solutions, in-depth analyses, and ensures reproducibility of findings. This material is part of an introductory graduate level course offered to students in plant and soil sciences with little or no coding experience. Anyone with sufficient motivation, discipline, and interest in learning how to code and adopt reproducible research practices should (hopefully) find this content useful. The material is aimed at students that are transitioning from spreadsheets analysis to a programming environemnt and that first need to learn basic building blocks before tackling more complicated challenges. With most datasets in a tabular format, the material is easily accessible and inspectable using common spreadsheet software.\nI selected Python because of its accessibility (it’s free), straightforward syntax, multi-purpose applications (data analysis, desktop applications, websites, games), widespread adoption in the scientific community, and a rich ecosystem of tools for reproducible research that makes the transition into the coding world a lot easier to beginners. The code presented here is complemented by live coding lectures and might not always reflect the most efficient or ‘pythonic’ methods. The goal is to present clear and explicit code to gradually familiarize students with the Python syntax, documentation resources, and improve the process of breaking down problems into a sequence of smaller logical steps to ultimately reach more advanced and elegant coding. This book strives for a balanced approach, blending task complexity with a judicious use of libraries. While libraries enhance reproducibility and benefit from extensive testing of the community, an overreliance on them can hinder beginners from truly grasping the underlying concepts and logic of programming.\nThe motivation for these notes stem from the need to increase coding literacy in students pursuing a career in agricultural sciences. With the rise of sensor technology and the expanding volume of data in agronomic decision-making, scientific programming has become indispensable for agriscientists analyzing, interpreting, and communicating data and research findings. This material addresses three key gaps:\n\nA scarcity of online resources with real agricultural datasets. This series uses data from peer-reviewed studies and research projects, offering practical and applicable examples that bridge theory and practice.\nExisting coding resources often target either a general audience or advanced students in computer science, leaving a void for agronomy students and early career scientists in agricultural sciences that are new to coding.\nAiming to provide concise, interactive, and well-documented Jupyter notebooks, this material is readily accessible via platforms like Github and Binder.\n\nDuring my own journey in graduate school, coding was a powerful tool for enhancing logical thinking, deconstructing complex problems into manageable steps, and sharpening my focus on details that initially seemed inconsequential. Code brought to life abstract concepts and equations that appeared in manuscripts and books, making intricate processes more tangible and comprehensible.\nAs a faculty member, coding has reshaped the way I interact with students. Code reveals the student’s reasoning process and allows me to connect with the student logical (or sometimes illogical) thought process. This way I can somewhat get into the student’s head and correct misconceptions. Collaborative coding has become one of the most fulfilling aspects of my academic career.\nThe goal of this book is to make you a competent (based on the Dreyfus scale) python programmer, meaning that you will be able to automate simple tasks, analyze datasets, and create reproducible and logical code that supports scientific claims in the domain of agricultural sciences. Students who successfully complete the material will be able to:\n\nconstruct effective, well-documented, and error-free scripts and functions.\napply high-level programming to generate publication-quality figures and optimize simple models.\nfind information independently for self-teaching and problem solving.\nlearn good programming habits and basic reproducible research practices by following short exercises using real data.\n\nI truly hope you find both learning and enjoyment in these pages. Happy coding!\n\nAndres Patrignani Associate Professor in Soil Water Processes Department of Agronomy Kansas State University" + }, + { + "objectID": "index.html#feedback", + "href": "index.html#feedback", + "title": "PyNotes in Agriscience", + "section": "Feedback", + "text": "Feedback\nIf you encounter any errors or have suggestions for improvement, please, share your insights with me. For bug reports, code suggestions, or requests for new topics, please create an issue in the Github repository. This platform is ideal for collaborative discussion and tracking the progress of your suggestions. You can also contact me directly at andrespatrignani@ksu.edu." + }, + { + "objectID": "index.html#support", + "href": "index.html#support", + "title": "PyNotes in Agriscience", + "section": "Support", + "text": "Support\nThe content of this website is partially supported by the Kansas State University Open/Alternative Textbook Initiative" + }, + { + "objectID": "index.html#acknowledgments", + "href": "index.html#acknowledgments", + "title": "PyNotes in Agriscience", + "section": "Acknowledgments", + "text": "Acknowledgments\nThis book was enriched by the invaluable insights and encouragement from numerous faculty members and students. Their inspiration and constructive feedback have been pivotal in shaping the content you see today. I am deeply grateful for their contributions and the collaborative spirit that has permeated this endeavor over the past ten years." + }, + { + "objectID": "index.html#license", + "href": "index.html#license", + "title": "PyNotes in Agriscience", + "section": "License", + "text": "License\nAll the code in these Jupyter notebooks has been written entirely by the author unless noted otherwise. The entire material is available for free under the Creative Commons Attribution-NonCommercial-ShareAlike (CC BY-NC-SA) license" + }, + { + "objectID": "index.html#references", + "href": "index.html#references", + "title": "PyNotes in Agriscience", + "section": "References", + "text": "References\nDreyfus, S.E., 2004. The five-stage model of adult skill acquisition. Bulletin of science, technology & society, 24(3), pp.177-181. https://doi.org/10.1177/0270467604264992" + }, + { + "objectID": "markdown/inspiration.html#great-quotes", + "href": "markdown/inspiration.html#great-quotes", + "title": "2  Inspiration", + "section": "Great quotes", + "text": "Great quotes\nThese are great quotes obtained from the video and the official code.org website:\n\n“I think that great programming is not all that dissimilar to great art. Once you start thinking in concepts of programming it makes you a better person…as does learning a foreign language, as does learning math, as does learning how to read.“ —Jack Dorsey. Creator, Twitter. Founder and CEO, Square\n\n\n“Software touches all of these different things you use, and tech companies are revolutionizing all different areas of the world…from how we shop to how farming works, all these things that aren’t technical are being turned upside down by software. So being able to play in that universe really makes a difference.“ —Drew Houston. Founder & CEO, Dropbox\n\n\n“To prepare humanity for the next 100 years, we need more of our children to learn computer programming skills, regardless of their future profession. Along with reading and writing, the ability to program is going to define what an educated person is.“ —Salman Khan. Founder, Khan Academy\n\n\n“Learning to speak the language of information gives you the power to transform the world.“ —Peter Denning. Association of Computing Machinery, former President\n\n\n“Learning to write programs stretches your mind, and helps you think better, creates a way of thinking about things that I think is helpful in all domains.“ —Bill Gates. Chairman, Microsoft\n\n\n“The programmers of tomorrow are the wizards of the future. You’re going to look like you have magic powers compared to everybody else“ —Gabe Newell. Founder and President, Valve" + }, + { + "objectID": "markdown/reproducible_research.html#jupyter-notebooks", + "href": "markdown/reproducible_research.html#jupyter-notebooks", + "title": "3  Reproducible Research", + "section": "Jupyter Notebooks", + "text": "Jupyter Notebooks\nA Jupyter notebook is a web-based environment for interactive computing. Jupyter notebooks seamlessly aggregate executable code, comments, equations, images, references, and paths or URL links to specific datasets within a single platform. In a Jupyter notebook, the code is neatly compartmentalized into cells, offering an organized and intuitive structure for coding. These cells are the cornerstone of a Jupyter Notebook’s functionality, allowing for the execution of individual code segments (activated by pressing ctrl + enter) independently. This feature enables coders to test and validate each block of code separately, ensuring its functionality and correctness before proceeding to subsequent sections. This modular approach to code execution not only enhances the debugging process but also improves the overall development workflow.\n\n\n\n\nGraphical user interface of Jupyter Lab notebooks." + }, + { + "objectID": "markdown/reproducible_research.html#references-and-recommended-reading", + "href": "markdown/reproducible_research.html#references-and-recommended-reading", + "title": "3  Reproducible Research", + "section": "References and recommended reading", + "text": "References and recommended reading\nGuo, P., 2013. Helping scientists, engineers to work up to 100 times faster. Link\nShen, H., 2014. Interactive notebooks: Sharing the code. Nature, 515(7525), pp.151-152. Link\nSandve, G.K., Nekrutenko, A., Taylor, J. and Hovig, E., 2013. Ten simple rules for reproducible computational research. PLoS computational biology, 9(10). Link\nSkaggs, T.H., Young, M.H. and Vrugt, J.A., 2015. Reproducible research in vadose zone sciences. Vadose Zone Journal, 14(10). Link" + }, + { + "objectID": "markdown/reproducible_research.html#reproducible-research-questions", + "href": "markdown/reproducible_research.html#reproducible-research-questions", + "title": "3  Reproducible Research", + "section": "Reproducible research questions", + "text": "Reproducible research questions\nBased on the reading of Guo 2013 and Skaggs et al., 2015, answer the following questions:\nQ1. List and briefly explain all the softwares that you used in the past 3 years for data analysis as part of your research.\nQ2. Briefly define what is reproducible research?\nQ3. Name 3 reasons why you need to learn coding as a scientist or engineer.\nQ4. How do you feel about sharing your data and code with the rest of the scientific community when publishing an article? Do you have any concerns?" + }, + { + "objectID": "markdown/coding_guidelines.html#references", + "href": "markdown/coding_guidelines.html#references", + "title": "4  Coding Guidelines", + "section": "References", + "text": "References\nVan Rossum, G., Warsaw, B. and Coghlan, N., 2001. PEP 8: style guide for Python code. Python. org, 1565. The official Python style guide (PEP 8) can be found here." + }, + { + "objectID": "markdown/installing_software.html#anaconda-package", + "href": "markdown/installing_software.html#anaconda-package", + "title": "5  Installing packages", + "section": "Anaconda package", + "text": "Anaconda package\nWe will use the Anaconda environment, which is a set of curated python packages commonly used in science and engineering. The Anaconda environment is available for free by Continuum Analytics.\n\nStep 1: Download the Anaconda installer\n\nDownload the Anaconda package for your platform (Windows, Mac, Linux)\n\n\n\nStep 2: Install Anaconda\n\nDouble click on the installer and follow the steps. When asked, I highly suggest installing VS Code, which is a powerful editor with autocomplition, debugging capabilities, etc.\nIn case you are having trouble, visit the Anaconda “Frequently Asked Questions” for some tips on how to troubleshoot most common issues: https://docs.anaconda.com/anaconda/user-guide/faq/\n\n\n\nStep 3: Open the Anaconda Navigator\n\nIn Windows go to the start up menu in the bottom left corner of the screen and then click on the Anaconda Navigator.\nIn Macs go to Applications and double click on the Anaconda Navigator. Alternatively you can use the search bar (press Command + Space bar and search for terminal).\nJupyterLab and Jupyter Notebook: We will write most of our code using notebooks, which are ideal for reproducible research.\nVS Code: A powerful and modern code editor. You can download it and code here if you want.\nSpyder: An integrated development environment for scientific coding in Python. It features a graphical user interface similar to that of Matlab.\n\n\n\n\n\n\n\nNote\n\n\n\nIf you open a notebook and run the pip list command, you can print all the installed packages in your Anaconda environment." + }, + { + "objectID": "markdown/installing_software.html#git", + "href": "markdown/installing_software.html#git", + "title": "5  Installing packages", + "section": "Git", + "text": "Git\nWhat is Git?\nGit is a distributed version control system that enables multiple users to track and manage changes to code and documents\nHow do I get started with Git?\nIf you have a Mac, you most likely already have Git installed. If you have a Windows machine or need to installed it for your Mac, follow these steps:\n\nGo to: https://git-scm.com\nSelect Windows/MacOS\nFollow the installer and use default intallation settings\nWe will most use the command window (called Git Bash), but we need it in order to work with Github." + }, + { + "objectID": "markdown/installing_software.html#github", + "href": "markdown/installing_software.html#github", + "title": "5  Installing packages", + "section": "Github", + "text": "Github\nWhat is Github?\n\nGitHub is a web platform that hosts Git repositories, offering tools for collaboration, code review, and project management. In addition to Github, there are other similar platforms such as Bitbucket and GitLab.\n\nHow do I get started with Github?\n\nCreate a Github account at: https://github.com/\nCreate a repository. Make sure to add a README file.\nGo to your computer and open the terminal\nNavigate to a directory where you want to place the repository\nClone the Github repository using: git clone <link>\n\nThese are just a few short instructions. Check out the detailed and more extensive tutorial to get started." + }, + { + "objectID": "markdown/installing_software.html#datasets", + "href": "markdown/installing_software.html#datasets", + "title": "5  Installing packages", + "section": "Datasets", + "text": "Datasets\nMost examples and exercises in the book use real datasets, which can be found in the /datasets directory of the Github repository. You can download the entire reporsitoy, a specific file, or simply read the file using the “Raw” URL link. For example, to read the daily weather dataset for the Kings Creek watershed named kings_creek_2022_2023_daily.csv you can run the following command:\npd.read_csv(https://raw.githubusercontent.com/andres-patrignani/harvestingdatawithpython/main/datasets/kings_creek_2022_2023_daily.csv)" + }, + { + "objectID": "markdown/terminal_commands.html#general-commands-mac-terminal-or-git-bash", + "href": "markdown/terminal_commands.html#general-commands-mac-terminal-or-git-bash", + "title": "6  Useful Terminal Commands", + "section": "General commands (Mac terminal or Git Bash)", + "text": "General commands (Mac terminal or Git Bash)\ncd <foldername> change directory; navigate into a new folder (assuming the folder exists in the directory).\ncd .. To navigate out of the current directory\nls To list the content of the folder\npwd full path to current directory\nSee image below where I ran these commands:" + }, + { + "objectID": "markdown/terminal_commands.html#useful-shortcuts", + "href": "markdown/terminal_commands.html#useful-shortcuts", + "title": "6  Useful Terminal Commands", + "section": "Useful shortcuts", + "text": "Useful shortcuts\nUse Tab key to autocomplete directory and file names\nUse arrow-up to access the last command\nIn Macs use cmd + v to paste text into the terminal\nIn Windows machines use shift + insert to paste text into Git bash" + }, + { + "objectID": "markdown/git_commands.html", + "href": "markdown/git_commands.html", + "title": "7  Useful Git commands", + "section": "", + "text": "Some of the most widely used git terminal commands. You can run the commands below anywhere, you don’t need to be inside any specific directory or repository. Some of these commands will change information in Git’s configuration file, so that you don’t have to re-type your username and email everytime you make a commit.\ngit config --global user.name \"john-doe\" In this case my username is: john-doe. The dash in the middle is part of the username.\ngit config --global user.email johndoe@abc.org In this case see that I did not include the email between quotation marks as I did with the username.\ngit config --global core.editor \"nano\" changes the default editor from “vim” to “nano”. In my opinion nano is a bit more friendly for beginners. Exit nano by pressing ctrl + X\n.gitignore contains file and folder names that you don’t want to keep track of version control. In other words, they will not sync with Github. If you added a rule in the .gitignore file after the file or folder has been added to your Github, you will need to erase the cache of the repository and then add the files again, so that changes take effect. You can do this following these commands: git rm --cached -r . and then git add .\ngit clone <repository link>: Clone repository into your local computer or a remote server. You only clone your repository once. If you work on a server or supercomputer, cloning a repository from a cloud-based platform like Github is much easier than transferring files using the terminal or copy pasting files using a graphical user interface like FileZilla.\ngit status: This command provides the state of the current repository in the local computer (or server) relative to Github’s remote repository.\ngit add .: Adds and removes all new file additions/deletions from repository. Remember, when you add or remove a new file to a folder in your local computer, it does not get automatically added to your repository. You have to execute the command for this to happen.\ngit add <filename> In case you want to add a single file to your repository. <filename> could be mytextfile.txt\ngit commit -a -m \"<write here a short descriptive message>\": Send changes to remote repository. Messages are mandatory and should be short and descriptive. Don’t accumulate too many changes before committing, otherwise your message/comment will not be meaningful to all the changes you made.\ngit push origin master Upload changes to master branch in the Github repository. To see changes make sure you refresh the Github webpage.\ngit pull origin master: Downloads updates in the master branch.\ngit checkout -- .: Disregard changes.\ngit checkout <branch name>: Changes scope to any branch, including the master branch. This command assumes that you have a branch." + }, + { + "objectID": "markdown/my_first_repo.html", + "href": "markdown/my_first_repo.html", + "title": "8  First Github repository", + "section": "", + "text": "This example will guide you through the step by step creation of a Github repository. Before we start, it will be helpful if we define some fo the jargon involved in the Git commands:\nclone: Means to download or copy an entire repository. push: Send updates/changes to Gihub repository. You need to clone the repository first. pull: Get updates from Github repository\n\nInstall the latest version of Git in your computer.\nCreate a Github account at github.com and log in.\nCreate a new repository\n\nTypical repository naming convention is: this-my-first-repo\nMake sure to add a README file\n\nThen, copy the download link.\nIn your computer, open the terminal or GitBash and navigate to the directory in which you would like to download (clone) your repository.\nWrite: git clone <link> to clone the repository to your computer. Replace <link> with the link that you copy in the previous step using cmd + v in macs and ctrl + insert in Windows machines.\nNow the repository is in your computer (and of course in Github, we did it in step 3). > Important: To do the following push or pull commands you first need to clone your repository.\nAt this point there is only one file in the repository, the README.md file. md files are basically text files with some markup formatting. Github will render Markdown text in this file and display it as the landing page of your repository.\nAs an example, let’s try to edit the README file. This statement is tricky because it is unclear whether I’m asking you to edit the README file in your local computer or to do so in Github. Yes, you can edit files in Github (at least text files). Let’s edit the README file in your computer.\nOpen the README file in a Jupyter Lab, Jupyter Notebook, or a regular text editor. Add some text to it. Don’t worry about adding Markdown format, just add some content.\nSave the modifications.\nNow the README file in your computer has modifications that the README file in Github doesn’t. Github does not sync automatically (like Dropbox). So, we need to find a way to send the changes to Github.\nIn your computer, use the terminal to navigate inside your repository.\nRun the following commands in the terminal. Press the enter key after writing each command. Enter Github account credentials if needed:\n\ngit add .\ngit commit -a -m \"Updated README file\"\ngit push\n\nWe didn’t really add a new file, so the first command is not required here, but it does not hurt to add it (at least in most scenarios when you are getting started).\n\n\nGo to your Github account and refresh the page. Since Github renders the content in the README file as the landing page of your repository, you should be able to see the changes right in front of your eyes.\nFollow step 13 and 14 to send future updates yo your Github repository.\nIf in in step 9 you decided to edit the README file directly in Github, then the situation would be the opposite. You would have changes in the README file in your Github account that aren’t in your local computer. To get the updates into your local computer, open the terminal and navigate to your repository and type git pull. Open the README file with a text editor and you should see the changes you made in the Github website.\nSometimes there are files that you just want to have them in your local computer, but don’t want to upload to your Github reposity. These files may include sensitive data, large files (>100 MB), or temporary files created by applications. So, to prevent Git syncing these files we a list of files that we want Git to ignore. This file is called the .gitignore file. To create a .gitignore file open a Jupyter Lab and go to: File -> New -> Text File. Click rename, delete the entire filename (make sure you also delete the .md part), and then name it .gitignore. The leading period is important. Make sure that there isn’t a trailing .txt extension. This means that this is hidden file. The Jupyter Lab navigation panel will not display hidden files. Just search on the web how to make hidden files visible in Windows Explorer or Mac Finder. The .gitignore file will appear in both your local directory and your Github repository. > Note that if you first add undesirable files to your repository (using git add, commit, and push), adding the .gitignore will not delete them from your Github account. If you are in this situation just insert: git rm --cached -r . and then git add .\n\nExample .gitignore file Text between square brackets is a comment and you shouldn’t write it into your .gitignore file.\n.DS_Store \n**/.ipynb_checkpoints/ \n/Private notebooks" + }, + { + "objectID": "markdown/markdown_basics.html#comments", + "href": "markdown/markdown_basics.html#comments", + "title": "9  Markdown basics", + "section": "Comments", + "text": "Comments\nMarkdown does not seem to have an official way of adding comments. However, we can fool several Markdown interpreters by preceding text with the following expression [//]:\n[//]: This is a comment\nNote that this trick might not work in some Markdown editors like Typora, but it does seem to work in Github." + }, + { + "objectID": "markdown/markdown_basics.html#line-breaks", + "href": "markdown/markdown_basics.html#line-breaks", + "title": "9  Markdown basics", + "section": "Line breaks", + "text": "Line breaks\nPressing the enter key will not generate empty lines. Because Markdown eventually is converted into HTML, we can use HTML tags to expand the editing and styling possibilities in our document. So, to add a line break, we can use the self-closing line break tag: <br/>.\nsome text\n</br>\nmore text" + }, + { + "objectID": "markdown/markdown_basics.html#headers", + "href": "markdown/markdown_basics.html#headers", + "title": "9  Markdown basics", + "section": "Headers", + "text": "Headers\nRepresented by adding 1 to 6 leading # signs\n# Title header\n## Sub-title header\n### Sub-sub-title header" + }, + { + "objectID": "markdown/markdown_basics.html#sub-title-header", + "href": "markdown/markdown_basics.html#sub-title-header", + "title": "9  Markdown basics", + "section": "Sub-title header", + "text": "Sub-title header\n\nSub-sub-title header" + }, + { + "objectID": "markdown/markdown_basics.html#emphasis", + "href": "markdown/markdown_basics.html#emphasis", + "title": "9  Markdown basics", + "section": "Emphasis", + "text": "Emphasis\n*italic text*\n_italic text_\n**bold text**\n__bold text__\n~~striked text~~\nitalic text\nitalic text\nbold text\nbold text\ntext" + }, + { + "objectID": "markdown/markdown_basics.html#highlighting", + "href": "markdown/markdown_basics.html#highlighting", + "title": "9  Markdown basics", + "section": "Highlighting", + "text": "Highlighting\nTo calculate the `sin(90)` first import the `math` module`\nTo calculate the sin(90) first import the math module`" + }, + { + "objectID": "markdown/markdown_basics.html#monospace-font", + "href": "markdown/markdown_basics.html#monospace-font", + "title": "9  Markdown basics", + "section": "Monospace font", + "text": "Monospace font\nIndent text using the Tab key to generate a monospace font." + }, + { + "objectID": "markdown/markdown_basics.html#inline-equations", + "href": "markdown/markdown_basics.html#inline-equations", + "title": "9  Markdown basics", + "section": "Inline equations", + "text": "Inline equations\n$y = ax+b$\ny = ax+b" + }, + { + "objectID": "markdown/markdown_basics.html#block-equations", + "href": "markdown/markdown_basics.html#block-equations", + "title": "9  Markdown basics", + "section": "Block equations", + "text": "Block equations\nExample equation for calculating actual vapor pressure (Eq. 17, FAO-56):\n$$ea = \\frac{eTmin\\frac{RHmax}{100}+eTmax\\frac{RHmin}{100}}{2}$$ \nea = \\frac{eTmin\\frac{RHmax}{100}+eTmax\\frac{RHmin}{100}}{2}​\nea = actual vapor pressure (kPa)\n\neTmax = saturation vapor pressure at temp Tmax (kPa)\n\neTmin = saturation vapor pressure at temp Tmin (kPa)\n\nRHmax = maximum relative humidity (%)\n\nRHmin = minimum relative humidity (%)" + }, + { + "objectID": "markdown/markdown_basics.html#block-quotes", + "href": "markdown/markdown_basics.html#block-quotes", + "title": "9  Markdown basics", + "section": "Block quotes", + "text": "Block quotes\nUse the > character to generate block quotes.\n>\"The programmers of tomorrow are the wizards of the future. You're going to look like you have magic powers compared to everybody else.\" *- Gabe Newell*\n\n“The programmers of tomorrow are the wizards of the future. You’re going to look like you have magic powers compared to everybody else.” - Gabe Newell" + }, + { + "objectID": "markdown/markdown_basics.html#bullet-lists", + "href": "markdown/markdown_basics.html#bullet-lists", + "title": "9  Markdown basics", + "section": "Bullet lists", + "text": "Bullet lists\nAny of these two alternatives:\n- item 1 * item 1\n- item 2 * item 2\n- item 3 * item 3\nwill generate something similar to this: - item 1 - item 2 - item 3" + }, + { + "objectID": "markdown/markdown_basics.html#numbered-lists", + "href": "markdown/markdown_basics.html#numbered-lists", + "title": "9  Markdown basics", + "section": "Numbered Lists", + "text": "Numbered Lists\n1. item 1\n2. item 2\n3. item 3\n\nitem 1\nitem 2\nitem 3" + }, + { + "objectID": "markdown/markdown_basics.html#in-line-links", + "href": "markdown/markdown_basics.html#in-line-links", + "title": "9  Markdown basics", + "section": "In-line links", + "text": "In-line links\n[Github-flavored markdown(https://www.wikiwand.com/en/Home_page)\nGithub-flavored markdown" + }, + { + "objectID": "markdown/markdown_basics.html#referenced-links", + "href": "markdown/markdown_basics.html#referenced-links", + "title": "9  Markdown basics", + "section": "Referenced links", + "text": "Referenced links\n[Try a live Markdown editor in your browser][1]\n\nSome text\nSome more text\n\n[1] https://stackedit.io \"Optional title to identify your source\"\nTry a live Markdown editor in your browser" + }, + { + "objectID": "markdown/markdown_basics.html#figures", + "href": "markdown/markdown_basics.html#figures", + "title": "9  Markdown basics", + "section": "Figures", + "text": "Figures\nFigures can be inserted in Markdown following this syntax:\n![alt_text](https://path_to_my_image/image.jpg \"My image\")\nBecause we many times want to deploy our Markdown in Github, then using pure HTML is the best option:\n<img src=\"upload.wikimedia.org/wikipedia/en/8/80/Wikipedia-logo-v2.svg\" alt=\"wikipedia_logo\" width=\"100\"/>" + }, + { + "objectID": "markdown/markdown_basics.html#horizontal-lines", + "href": "markdown/markdown_basics.html#horizontal-lines", + "title": "9  Markdown basics", + "section": "Horizontal lines", + "text": "Horizontal lines\nYou can use three consecutive dashes, astericks, or underscores in this fashion:\n---\n***\n___\nFor instance, typing ---, we obtain the following line:" + }, + { + "objectID": "markdown/markdown_basics.html#code", + "href": "markdown/markdown_basics.html#code", + "title": "9  Markdown basics", + "section": "Code", + "text": "Code\nWe can write inline or block code. Inline code:\n`s = \"Python inline code syntax highlighting\"`\ns = \"Python inline code syntax highlighting\"\nand block code:\n​```python\n# Creating a matrix or 2D array\nM = [[1, 4, 5],\n [-5, 8, 9]]\nprint(M)\n​```\n# Creating a matrix or 2D array\nM = [[1, 4, 5],\n [-5, 8, 9]]\nprint(M)" + }, + { + "objectID": "markdown/markdown_basics.html#tables", + "href": "markdown/markdown_basics.html#tables", + "title": "9  Markdown basics", + "section": "Tables", + "text": "Tables\nSimple tables are easy to write in Markdown. However, adding more than a handful of rows and/or columns can turn out to be a pain. So, if you want to display many lines I suggest using a Markdown table generator. Some Markdown editors have shortcuts and table generators and there are websites exclusively dedicated to generate Markdown tables. Below I show a trivial example:\n| Textural class | Sand (%) | Clay (%) |\n|:----------------|:--------:|:--------:|\n| Silty clay loam | 10 | 35 |\n| Sandy loam | 60 | 15 |\n| Clay loam | 35 | 35 |\nThe leftmost column is left-aligned :---, the center column is center-aligned :---:, and the righmost column is right-aligned ---:. The | characters don’t need to be aligned in order for the Mardown interpreter to properly render the table, but it certainly helps while constructing the table by hand.\n\n\n\nTextural class\nSand (%)\nClay (%)\n\n\n\n\nSilty clay loam\n10\n35\n\n\nSandy loam\n60\n15\n\n\nClay loam\n35\n35" + }, + { + "objectID": "markdown/latex_equations.html#examples", + "href": "markdown/latex_equations.html#examples", + "title": "10  LaTeX equations", + "section": "Examples", + "text": "Examples\nBelow is a set of equations obtained from the FAO 56 manual to calculate reference evapotranspiration. Use this equations as templates to learn how to implement your own equations.\n\nReference Evapotranspiration Equation\n$$ETo = \\frac{0.408\\Delta(Rn-G)+\\gamma\\frac{900}{T+273}u2(es-ea)}{\\Delta+\\gamma(1+0.34u2)}$$\nETo = \\frac{0.408\\Delta(Rn-G)+\\gamma\\frac{900}{T+273}u2(es-ea)}{\\Delta+\\gamma(1+0.34u2)}\nETo = reference evapotranspiration (mm/day)\nRn = net radiation at the crop surface (MJ/m2/day)\nG = soil heat flux density (MJ/m2/day)\nT = mean daily air temperature at 2 m height\nu2 = wind speed at 2 m height (m/s)\nes = saturation vapor pressure (kPa)\nea = actual vapor pressure (kPa)\nes-ea = saturation vapor pressure deficit (kPa)\n\\Delta = slope vapor pressure curve (kPa/°C)\n\\gamma = psychrometric constant (kPa/°C)\n\n\nPsychrometric constant\n$$\\gamma = \\frac{Cp P}{\\epsilon \\lambda}$$\n\\gamma = \\frac{Cp \\ P}{\\epsilon \\lambda}\n\\gamma = psychrometric constant (kPa/°C)\n\\lambda = latent heat of vaporization, 2.45 (MJ/kg)\nCp = specific heat at constant pressure (MJ/kg/°C)\n\\epsilon = ratio of molecular weight of water vapour/dry air = 0.622\nP = atmospheric pressure (kPa)\n\n\n\nWind speed at 2 meters above the soil surface\n$$u2 = uz\\frac{4.87}{\\ln(67.8z-5.42)}$$\nu2 = uz\\frac{4.87}{\\ln(67.8z-5.42)}\nu2 = wind speed at 2 m above ground surface (m/s)\nuz = measured wind speed at z m above ground surface (m/s)\nzm = height of measurement above ground surface (m)\n\n\nMean saturation vapor pressure\n$$es = \\frac{eTmax+eTmin}{2}$$\nes = \\frac{eTmax+eTmin}{2}\nes = mean saturation vapor pressure (kPa)\neTmax = saturation vapor pressure at temp Tmax (kPa)\neTmin = saturation vapor pressure at temp Tmin (kPa)\n\n\nSlope of vapor pressure\n$$\\Delta = \\frac{4098\\bigg[0.6108\\exp\\bigg(\\frac{17.27 Tmean}{Tmean+237.3}\\bigg)\\bigg]}{(Tmean+237.3)^2}$$\n\\Delta = \\frac{4098\\bigg[0.6108\\exp\\bigg(\\frac{17.27 Tmean}{Tmean+237.3}\\bigg)\\bigg]}{(Tmean+237.3)^2}\n\\Delta = slope of saturation vapor pressure curve at air temp T (kPa/°C)\nTmean = average daily air temperture\n\n\nActual vapor pressure\n$$ea = \\frac{eTmin\\frac{RHmax}{100}+eTmax\\frac{RHmin}{100}}{2}$$\nea = \\frac{eTmin\\frac{RHmax}{100}+eTmax\\frac{RHmin}{100}}{2}\nea = actual vapor pressure (kPa)\neTmax = saturation vapor pressure at temp Tmax (kPa)\neTmin = saturation vapor pressure at temp Tmin (kPa)\nRHmax = maximum relative humidity (%)\nRHmin = minimum relative humidity (%)\n\n\nExtraterrestrial solar radiation\n$$Ra=\\frac{24(60)}{\\pi}\\hspace{2mm}G\\hspace{2mm}dr[\\omega\\sin(\\phi)\\sin(\\delta)+\\cos(\\phi)\\cos(\\delta)\\sin(\\omega)]$$\nRa = \\frac{24(60)}{\\pi} \\hspace{2mm}G \\hspace{2mm} dr[\\omega\\sin(\\phi)\\sin(\\delta)+\\cos(\\phi)\\cos(\\delta)\\sin(\\omega)]\nRa = extraterrestrial radiation (MJ/m2/day)\nG = solar constant (MJ/m2/min)\ndr = 1 + 0.033 \\cos(2\\pi J/365)\nJ = number of the day of the year\n\\phi = \\pi/180 decimal degrees (latitude in radians)\n\\delta = 0.409\\sin((2\\pi J/365)-1.39)\\hspace{5mm} Solar decimation (rad)\n\\omega = \\pi/2-(\\arccos(-\\tan(\\phi)\\tan(\\delta)) \\hspace{5mm} sunset hour angle (radians)" + }, + { + "objectID": "markdown/alt_python_libraries.html#biopython", + "href": "markdown/alt_python_libraries.html#biopython", + "title": "11  Python libraries", + "section": "Biopython", + "text": "Biopython\nMature module for biological computation developed and maintained by a global community. I suggest inspecting the tutorial and the cookbook examples. Great, clean documentation.\nOfficial page: https://biopython.org/" + }, + { + "objectID": "markdown/alt_python_libraries.html#earthpy", + "href": "markdown/alt_python_libraries.html#earthpy", + "title": "11  Python libraries", + "section": "EarthPy", + "text": "EarthPy\nCollection of IPython notebooks with examples of Earth Science. The module was developed and is maintained by Nikolay Koldunov. Examples are available in the “Dataprocessing” tab.\nOfficial page: http://earthpy.org/" + }, + { + "objectID": "markdown/alt_python_libraries.html#metpy", + "href": "markdown/alt_python_libraries.html#metpy", + "title": "11  Python libraries", + "section": "MetPy", + "text": "MetPy\nOpen Source project for meteorological data analysis.\nOfficial page: https://unidata.github.io/MetPy/latest/" + }, + { + "objectID": "markdown/alt_python_libraries.html#pysheds", + "href": "markdown/alt_python_libraries.html#pysheds", + "title": "11  Python libraries", + "section": "pysheds", + "text": "pysheds\nLibrary for watershed delineation in Python.\nSite: https://github.com/mdbartos/pysheds" + }, + { + "objectID": "markdown/alt_python_libraries.html#whitebox", + "href": "markdown/alt_python_libraries.html#whitebox", + "title": "11  Python libraries", + "section": "whitebox", + "text": "whitebox\nPython package to perform common geographical information systems analysis operations such as cost-distance analysis, distance buffering, and raster reclassification. It also has a GUI interface.\nOfficial site: https://github.com/giswqs/whitebox" + }, + { + "objectID": "markdown/alt_python_libraries.html#pastas", + "href": "markdown/alt_python_libraries.html#pastas", + "title": "11  Python libraries", + "section": "pastas", + "text": "pastas\nPython package for processing, simulating and analyzing hydrological time series.\nOfficial site: https://github.com/pastas/pastas" + }, + { + "objectID": "markdown/alt_python_libraries.html#rasterio", + "href": "markdown/alt_python_libraries.html#rasterio", + "title": "11  Python libraries", + "section": "Rasterio", + "text": "Rasterio\nLibrary that allows you to read, organize, and store gridded raster datasets such as satellite imagery and terrain models in GeoTIFF and other formats.\nOfficial site: https://rasterio.readthedocs.io/en/latest/" + }, + { + "objectID": "markdown/alt_python_libraries.html#xarray", + "href": "markdown/alt_python_libraries.html#xarray", + "title": "11  Python libraries", + "section": "xarray", + "text": "xarray\nPython project for working with labelled multi-dimensional arrays.\nOfficial site: http://xarray.pydata.org/en/stable/" + }, + { + "objectID": "markdown/alt_python_libraries.html#sklearn", + "href": "markdown/alt_python_libraries.html#sklearn", + "title": "11  Python libraries", + "section": "sklearn", + "text": "sklearn\nData mining, data analysis, and machine learning library to solve classification, regression, and clustering problems.\nOfficial site: https://scikit-learn.org/stable/" + }, + { + "objectID": "basic_concepts/basic_operations.html#hello-world", + "href": "basic_concepts/basic_operations.html#hello-world", + "title": "12  Basic operations", + "section": "Hello World", + "text": "Hello World\n\n# The most iconic line of code when learning how to program\nprint(\"Hello World\")\n\nHello World" + }, + { + "objectID": "basic_concepts/basic_operations.html#comments", + "href": "basic_concepts/basic_operations.html#comments", + "title": "12  Basic operations", + "section": "Comments", + "text": "Comments\nIn Python, as in any programming language, comments play a crucial role in enhancing the clarity, readability, and maintainability of code. Comments are used to annotate various parts of the code to provide context or explain the purpose of specific blocks or lines. This is especially important in Python, where the emphasis on clean and readable code aligns with the language’s philosophy. Comments are a mark of good programming practice and help both the original author and other programmers who may work with the code in the future to quickly understand the intentions, logic, and functionality of the code.\nIn Python, a comment is created by placing the hash symbol # (a.k.a. pound sign, number sign, sharp, or octothorpe) before any text or number. Anything following the # on the same line is ignored by the Python interpreter, allowing programmers to include notes and explanations directly in their code.\n\n# This line is a comment and will be ignored by the Python interpreter.\nprint(\"This sentence will print\") # This sentence will not print\n\nThis sentence will print\n\n\n\n\n\n\n\n\nTip\n\n\n\nComments can be used to disable code lines without deleting them. This is especially handy when experimenting with different solutions. You can retain alternative code snippets as comments, which will be ignored by the interpreter, but remain available for reference or reactivation at a later time." + }, + { + "objectID": "basic_concepts/basic_operations.html#arithmetic-operations", + "href": "basic_concepts/basic_operations.html#arithmetic-operations", + "title": "12  Basic operations", + "section": "Arithmetic operations", + "text": "Arithmetic operations\nIn Python, arithmetic operators are fundamental tools used for performing basic mathematical operations. The most commonly used operators include addition (+), subtraction (-), multiplication (*), and division (/). Apart from these, Python also features the modulus operator (%) which returns the remainder of a division, the exponentiation operator (**) for raising a number to the power of another, and the floor division operator (//) which divides and rounds down to the nearest whole number. These operators are essential building blocks and form the basis of more complex mathematical functionality in the language.\n\nprint(11 + 2) # Addition\nprint(11 - 2) # Subtraction\nprint(11 * 2) # Multiplication\nprint(11 / 2) # Division\nprint(11 // 2) # Floor division\nprint(11**2) # Exponentiation\nprint(11 % 2) # Modulus\n\n13\n9\n22\n5.5\n5\n121\n1\n\n\n\n\n\n\n\n\nTip\n\n\n\nIn newer versions of Jupyter Lab you can run a single line of code within a cell using Run Selected Text or Current Line in Console in the Run menu.\n\n\n\n\n\n\n\n\nExponentiation\n\n\n\nIn Python, the exponentiation operation is performed using a double asterisk (**), which is a departure from some other programming languages, such as Matlab, where a caret (^) is used for this operation." + }, + { + "objectID": "basic_concepts/basic_operations.html#simple-computations", + "href": "basic_concepts/basic_operations.html#simple-computations", + "title": "12  Basic operations", + "section": "Simple computations", + "text": "Simple computations\nIn programming, a variable is like a storage box where you can keep data that you might want to use later in your code. Think of it as a named cell in an Excel spreadsheet. Just as you might store a number or a piece of text in a spreadsheet cell and refer to it by its cell address (like A1 or B2), in coding, you store data in a variable and refer to it by the name you have given it. Variables are essential because they allow us to handle data dynamically and efficiently in our programs.\nTo get comfortable with the language syntax, variable definition, data types, and operators let’s use Python to solve simple problems that we typically carry on a calculator. In the following examples you learn how to:\n\ncode and solve a simple arithmetic problem\ndocument a notebook using Markdown syntax\nembed LaTeX equations in the documentation\n\n\nExample 1: Unit conversions\nPerhaps one of the most common uses of calculators is to do unit conversions. In this brief example we will use Python to conver from degrees Fahrenheit to degrees Celsius:\n C = (F-32) \\frac{5}{9} \n\n\n\n\n\n\nNote\n\n\n\nAlways assign meaningful names to your variables. Variable names must start with a letter or an underscore and cannot contain spaces. Numbers can be included in variables names as long as they are preceded by a letter or underscore.\n\n\n\nvalue_in_fahrenheit = 45\nvalue_in_celsius = (value_in_fahrenheit-32) * 5/9\n\n# Print answer\nprint(\"The temperature is:\", round(value_in_celsius,2), \"°C\")\n\nThe temperature is: 7.22 °C\n\n\n\n\n\n\n\n\nHelp access\n\n\n\nThe print and round functions can take multiple arguments as inputs. In this context, the comma is a delimiter between the function inputs. Use the help() or ? command to access a brief version of the documentation of a function. For instance, both help(round) and round? will print the documentation for the round() function, showing the the first argument is the number we want to round, and the second argument is the number of decimal digits. The official documentation for the round() function is available at this link.\n\n\n\n# Access functon help\nround?\n\n\nSignature: round(number, ndigits=None)\nDocstring:\nRound a number to a given precision in decimal digits.\nThe return value is an integer if ndigits is omitted or None. Otherwise\nthe return value has the same type as the number. ndigits may be negative.\nType: builtin_function_or_method\n\n\n\n\n# Find current variables in workspace\n%whos\n\nVariable Type Data/Info\n----------------------------------------\nvalue_in_celsius float 7.222222222222222\nvalue_in_fahrenheit int 45\n\n\n\n\nExample 2: Compute the hypotenuse\nGiven that a and b are the sides of a right-angled triangle, let’s compute the hypotenuse c. For instance, if a = 3.0 and b = 4.0, then our code must return a value of c = 5.0 since:\n c = \\sqrt{a^2 + b^2} \n\na = 3.0 # value in cm\nb = 4.0 # value in cm\nhypotenuse = (a**2 + b**2)**(1/2)\n\n# Print answer\nprint('The hypotenuse is:', round(hypotenuse, 2), 'cm')\n\nThe hypotenuse is: 5.0 cm\n\n\nDid you notice that we used **(1/2) to represent the square root? Another alternative is to import the math module, which is part of the Python Standard Library, and extends the functionality of our program. To learn more, check the notebook about importing Python modules.\n\nimport math\nhypotenuse = math.sqrt((a**2 + b**2))\n\n# Print answer\nprint('The hypotenuse is:', round(hypotenuse, 2), 'cm')\n\nThe hypotenuse is: 5.0 cm\n\n\n\n# The math module also has a built-in function to compute the Euclidean norm or hypothenuse\nprint(math.hypot(a, b))\n\n5.0\n\n\n\n\nExample 3: Calculate soil pH\nSoil pH represents the H^+ concentration in the soil solution and indicates the level of acidity or alkalinity of the soil. It is defined on a scale of 0 to 14, with pH of 7 representing neutral conditions, values below 7 indicating acidity, and values above 7 indicating alkalinity. Soil pH influences the availability of nutrients to plants and affects soil microbial activity. It is typically measured using a pH meter or indicator paper strips, where soil samples are mixed with a distilled water and then tested.\nThe formula is: pH = -log_{10}(H^+)\n\nH_concentration = 0.0001\nsoil_ph = -math.log10(H_concentration)\nprint(soil_ph)\n\n4.0\n\n\n\n\nExample 4: Calculate bulk density\nThe bulk density is crucial soil physical property for understanding soil compaction and soil health. Assume that an undisturbed soil sample was collected using a ring with a diameter of 7.5 cm and a height of 5.0 cm. The soil was then oven-dried to remove all the water. Given a mass of dry soil (M_s) of 320 grams (excluding the mass of the ring), calculate the bulk density (\\rho_b) of the soil using the following formula: \\rho_b = M_s/V. In this case we assume that the ring was full of soil, so the volume of the ring is the volume of the soil under analysis.\n\ndry_soil = 320 # grams\nring_diamter = 7.5 # cm\nring_height = 5.0 # cm\nring_volume = math.pi * (ring_diamter/2)**2 * ring_height # cm^3\n\nbulk_density = dry_soil/ring_volume\n\n# Print answer\nprint('Bulk density =', round(bulk_density, 2), 'g/cm³')\n\nBulk density = 1.45 g/cm³\n\n\n\n\n\n\n\n\nTip\n\n\n\nIn Python 3, UTF-8 is the default character encoding, so Unicode characters can be used anywhere. This is why we can write °C and cm³. Here is an extensive list of Unicode characters\n\n\n\n\nExample 5: Compute grain yield\nComputing grain yield is probably one of the most common operations in the field of agronomy. In this example we will compute the grain yield for a corn field based on kernels per ear, the number of ears per plant, the number of plants per square meter, and the weight of 1,000 kernels. This example can be easily adapted to estimate the harvestable yield of other crops.\n\n# Define variables\nkernels_per_ear = 500\nears_per_plant = 1\nplants_per_m2 = 8\nweight_per_1000_kernels = 285 # in grams\n\n# Calculate total kernels per square meter\nkernels_per_m2 = kernels_per_ear * ears_per_plant * plants_per_m2\n\n# Calculate yield in grams per square meter \n# We divide by 1000 to compute groups of 1000 kernels\nyield_g_per_m2 = (kernels_per_m2 / 1000) * weight_per_1000_kernels\n\n# Convert yield to kilograms per hectare \n# Use (1 hectare = 10,000 square meters) and (1 metric ton = 1 Mg = 1,000,000 g)\nyield_tons_per_ha = yield_g_per_m2 * 10000 / 10**6\n\nprint(\"Corn grain yield in metric tons per hectare is:\", yield_tons_per_ha)\n\nCorn grain yield in metric tons per hectare is: 11.4" + }, + { + "objectID": "basic_concepts/basic_operations.html#python-is-synchronous", + "href": "basic_concepts/basic_operations.html#python-is-synchronous", + "title": "12  Basic operations", + "section": "Python is synchronous", + "text": "Python is synchronous\nAn important concept demonstrated by the time module is Python’s synchronous nature. The Python interpreter processes code line by line, only moving to the next line after the current one has completed execution. This sequential execution means that if a line of code requires significant time to run, the rest of the code must wait its turn. While this can cause delays, it also simplifies coding by allowing for a straightforward, logical sequence in scripting. With the sleep() method, we can introduce a time delay to simulate extended computations.\n\nprint('Executing step 1')\ntime.sleep(5) # in seconds\n\nprint('Executing step 2')\ntime.sleep(3) # in seconds\n\nprint('Executed step 3')\n\nprint('See, Python is synchronous!')\n\nExecuting step 1\nExecuting step 2\nExecuted step 3\nSee, Python is synchronous!" + }, + { + "objectID": "basic_concepts/basic_operations.html#practice", + "href": "basic_concepts/basic_operations.html#practice", + "title": "12  Basic operations", + "section": "Practice", + "text": "Practice\nCreate a notebook that solves the following problems. Make sure to document your notebook using Markdown and LaTeX.\n\nConvert 34 acres to hectares. Answer: 13.75 hectares\nCompute the slope (expressed as a percentage) between two points on a terrain that are 150 meters apart and have a difference in elevation of 12 meters. Answer: 8% slope\nCompute the time that it takes for a beam of sunlight to reach our planet. The Earth-Sun distance is 149,597,870 km and the speed of light in vacuum is 300,000 km per second. Express your answer in minutes and seconds. Answer: 8 minutes and 19 seconds\n\n\n\n\n\n\n\nSyntax tip\n\n\n\nCode readability is a core priority of the Python language. Since Python 3.6, now we can use underscores to make large numbers more readable. For instance, the distance to the sun could be written in Python as: 149_597_870. The Python interpreter will ignore the underscores at the time of performing computations." + }, + { + "objectID": "basic_concepts/import_modules.html#importing-module-syntax", + "href": "basic_concepts/import_modules.html#importing-module-syntax", + "title": "13  Import modules", + "section": "Importing module syntax", + "text": "Importing module syntax\nThere are multiple ways of importing Python modules depending on whether you want to import the entire module, assign a shorter alias, or import a specific sub-module. While the examples below could be applied to the same module, I chose to use typical examples that you will encounter for each option.\nOption 1 Syntax: import <module> Example: import math Use-case: This is the simplest form of importing and is used when you need to access multiple functions or components from a module. It’s commonly used for modules with short names that don’t require an alias. When using this form, you call the module’s components with the module name followed by a dot, e.g., math.sqrt().\n\nimport math\n\na = 3.0 # value in cm\nb = 4.0 # value in cm\nhypotenuse = math.sqrt((a**2 + b**2))\nprint('The hypotenuse is:', round(hypotenuse,2), 'cm')\n\nThe hypotenuse is: 5.0 cm\n\n\nHere is another example using this syntax. With the sys module we can easily check our python version\n\nimport sys\nprint(sys.version) # Useful to check your python version\n\n3.10.9 (main, Mar 1 2023, 12:33:47) [Clang 14.0.6 ]\n\n\nOption 2 Syntax: import <module> as <alias> Example: import numpy as np Use-case: This is used to import the entire module under a shorter, often more convenient alias. It’s particularly useful for frequently used modules or those with longer names. The alias acts as a shorthand, making the code more concise and easier to write. For instance, you can use np.array() instead of numpy.array().\n\nimport numpy as np\n\n# Create an Numpy array of sand content for different soils\nsand_content = np.array([5, 20, 35, 60, 80]) # Percentage\n\nprint(sand_content)\n\n[ 5 20 35 60 80]\n\n\nOption 3 Syntax: import <module>.<submodule> as <alias> Example: import matplotlib.pyplot as plt Use-case: This approach is used when you only need a specific part or submodule of a larger module. It’s efficient as it only loads the required submodule into memory. The alias is used for ease of reference, as in plt.plot() instead of using matplotlib.pyplot.plot(), which will be much more verbose and will result in cluttered lines of code.\n\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(4,4))\nplt.scatter(sand_content, bulk_density, facecolor='white', edgecolor='black')\nplt.xlabel('Sand content (%)')\nplt.ylabel('Soil porosity (%)')\nplt.show()\n\n\n\n\nOption 4 Syntax: from <module> import <submodule> as <alias> Example: from numpy import random as rnd Use-case: This method is used when you need only a specific component or function from a module. It’s the most specific and memory-efficient way of importing, as only the required component is loaded. This is useful for modules where only one or a few specific functions are needed, and it allows you to use these functions directly without prefixing them with the module name, such as using rand() instead of numpy.random(). Despite being more memory efficient, this option can lead to conflicts in the variable namespace. This can cause confusion if multiple imported elements share the same name, potentially overwriting each other. Additionally, this approach can obscure the origin of functions or components, reducing code readability.\n\nfrom numpy import random as rnd\n\n# Set seed for reproducibility (without this you will get different array values)\nrnd.seed(0)\n\n# Create a 5 by 5 matrix of random integers between 0 and 9\n# The randint function returns\nM = rnd.randint(0, 10, [5,5])\nprint(M)\n\n[[5 0 3 3 7]\n [9 3 5 2 4]\n [7 6 8 8 1]\n [6 7 7 8 1]\n [5 9 8 9 4]]\n\n\n\n\n\n\n\n\nNote\n\n\n\nUse the Python help to access the documentation of the randint() method by running the following command: rnd.randint? Can you see why we had to use a value of 10 for the second argument of the function?" + }, + { + "objectID": "basic_concepts/data_types.html#integers", + "href": "basic_concepts/data_types.html#integers", + "title": "14  Data types", + "section": "Integers", + "text": "Integers\n\nplants_per_m2 = 8\nprint(plants_per_m2)\nprint(type(plants_per_m2)) # This is class int\n\n8\n<class 'int'>" + }, + { + "objectID": "basic_concepts/data_types.html#floating-point", + "href": "basic_concepts/data_types.html#floating-point", + "title": "14  Data types", + "section": "Floating point", + "text": "Floating point\n\nrainfall_amount = 3.4 # inches\nprint(rainfall_amount)\nprint(type(rainfall_amount)) # This is class float (numbers with decimal places)\n\n3.4\n<class 'float'>" + }, + { + "objectID": "basic_concepts/data_types.html#strings", + "href": "basic_concepts/data_types.html#strings", + "title": "14  Data types", + "section": "Strings", + "text": "Strings\n\n# Strings are defined using single quotes ...\ncommon_name = 'Winter wheat'\n\n# ... or double quotes\nscientific_name = 'Triticum aestivum'\n\n# ... but do not mix them\n\nprint(common_name)\nprint(scientific_name)\n\nprint(type(common_name))\nprint(type(scientific_name))\n\nWinter wheat\nTriticum aestivum\n<class 'str'>\n<class 'str'>\n\n\n\n# For longer blocks that span multiple lines we can use triple quotes (''' or \"\"\")\n\nsoil_definition = \"\"\"The layer(s) of generally loose mineral and/or organic material \nthat are affected by physical, chemical, and/or biological processes\nat or near the planetary surface and usually hold liquids, gases, \nand biota and support plants.\"\"\"\n\nprint(soil_definition)\n\nThe layer(s) of generally loose mineral and/or organic material \nthat are affected by physical, chemical, and/or biological processes\nat or near the planetary surface and usually hold liquids, gases, \nand biota and support plants.\n\n\n\n\n\n\n\n\nNote\n\n\n\nThe multi-line string appears as separate lines due to hidden line breaks at the end of each line, like when we press the Enter key. These breaks are represented by \\n, which are not displayed, but can be used for splitting the long string into individual lines. Try the following line: soil_definition.splitlines(), which is equivalent to soil_definition.split('\\n')\n\n\n\n# Split a string\nfilename = 'corn_riley_2024.csv' # Filename with corn yield data for Riley county in 2024\n\n# Split the string and assign the result to different variables\n# This only works if the number of variables on the LHS matches the number of outputs\n# Try running filename.split('.') on its own to see the resulting list\nbase_filename, ext_filename = filename.split('.')\nprint(base_filename)\n\n# Now we can do the same, but splitting at the underscore\ncrop, county, year = base_filename.split('_')\n\nprint(crop)\n\ncorn_riley_2024\ncorn\n\n\n\n\n\n\n\n\nNote\n\n\n\nThe command base_filename, ext_filename = filename.split('.') splits the string at the ., and automatically assigns each of the resulting elements ('corn_riley_2024' and 'csv') to each variable on the left-hand side. If you only run filename.split('.') the result is a list with two elements: ['corn_riley_2024', 'csv']\n\n\n\n# Replace characteres\nprint(filename.replace('_', '-'))\n\ncorn-riley-2024.csv\n\n\n\n# Join strings using the `+` operator\nfilename = \"myfile\"\nextension = \".csv\"\npath = \"/User/Documents/Datasets/\"\n\nfullpath_file = path + filename + extension\nprint(fullpath_file)\n\n/User/Documents/Datasets/myfile.csv\n\n\n\n# Find if word starts with one of the following sequences\nprint(base_filename.startswith(('corn','maize'))) # Note that the input is a tuple\n\n# Find if word ends with one of the following sequences\nprint(base_filename.endswith(('2022','2023'))) # Note that the input is a tuple\n\nTrue\nFalse\n\n\n\n# Passing variables into strings\nstation = 'Manhattan'\nprecip_amount = 25\nprecip_units = 'mm'\n\n# Option 1 (preferred): f-string format (note the leading f)\noption_1 = f\"Today's Precipitation at the {station} station was {precip_amount} {precip_units}.\"\nprint(option_1)\n\n# Option 2: %-string\n# Note how much longer this syntax is. This also requires to keep track of the order of the variables\noption_2 = \"Today's Precipitation at the %s station was %s %s.\" % (station, precip_amount, precip_units)\nprint(option_2)\n\n# ... however this syntax can sometimes be handy.\n# Say you want to report parameter values using a label for one of your plots\npar_values = [0.3, 0.1, 120] # Three parameter values, a list typically obtained by curve fitting\nlabel = 'fit: a=%5.3f, b=%5.3f, c=%5.1f' % tuple(par_values)\nprint(label)\n\nToday's Precipitation at the Manhattan station was 25 mm.\nToday's Precipitation at the Manhattan station was 25 mm.\nfit: a=0.300, b=0.100, c=120.0\nDOY:A001\nDOY:A365\n\n\n\n# Formatting of values in strings\nsoil_pH = 6.7832\ncrop_name = \"corn\"\n\n# Using an f-string to embed variables and format the pH value\nmessage = f\"The soil pH suitable for growing {crop_name} is {soil_pH:.2f}.\"\n\nTo specify the number of decimals for a value in an f-string, you can use the colon : followed by a format specifier inside the curly braces {}. For example, {variable:.2f} will format the variable to two decimal places. In this example, {soil_pH:.2f} within the f-string takes the soil_pH variable and formats it to two decimal places. The :.2f part is the format specifier, where . indicates precision, 2 is the number of decimal places, and f denotes floating-point number formatting. This approach is highly efficient and readable, making f-strings a favorite among Python programmers.\n\n# Say that you want to download data using the url from the NASA-MODIS satellite for a specific day of the year\ndoy = 1\nprint(f\"DOY:A{doy:03d}\")\n\nDOY:A001\n\n\nIn the f-string f\"A{number:03d}\", {number:03d} formats the variable number. The 03d specifier means that the number should be padded with zeros to make it three digits long (d stands for ‘decimal integer’ and 03 means ‘three digits wide, padded with zeros’). The letter ‘A’ is added as a prefix directly in the string. So, if number is 1, it gets formatted as 001, and the complete string becomes A001.\n\n# Compare strings\nprint('loam' == 'Loam') # Returns False since case matters\n\nprint('loam' == 'Loam'.lower()) # Returns True since we convert the second word to lower case\n\nFalse\nTrue\n\n\n\n\n\n\n\n\nNote\n\n\n\nWhen comparing strings, using either lower() or upper() helps standarizing the strings before the boolean operation. This is particularly useful if you need to request information from users or deal with messy datasets that are not consistent." + }, + { + "objectID": "basic_concepts/data_types.html#booleans", + "href": "basic_concepts/data_types.html#booleans", + "title": "14  Data types", + "section": "Booleans", + "text": "Booleans\nBoolean data types represent one of two values: True or False. Booleans are particularly powerful when used with conditional statements like if. By evaluating a boolean expression in an if statement, you can control the flow of your program, allowing it to make decisions and execute different code based on certain conditions. For instance, an if statement can check if a condition is True, and only then execute a specific block of code. Check the section about if statements for some examples.\nBoolean logical operators or: Will evaluate to True if at least one (but not necessarily both) statements is True and: Will evaluate to True only if both statements are True not: Reverses the result of the statement\nBoolean comparison operators ==: equal !=: not equal >=: greater or equal than <=: less or equal than >: greater than <: less than \n\n\n\n\n\n\nNote\n\n\n\nPython evaluates conditional arguments from left to right. The evaluation halts as soon as the outcome is determined, and the resulting value is returned. Python does not evaluate subsequent operands unless it is necessary to resolve the result.\n\n\n\n# Example boolean logical operator\n\nadequate_moisture = True\nprint(adequate_moisture)\nprint(type(adequate_moisture))\n\nTrue\n<class 'bool'>\n\n\n\n# Example boolean comparison operators\noptimal_moisture_level = 30 # optimal soil moisture level as a percentage\ncurrent_moisture_level = 25 # current soil moisture level as a percentage\n\nis_moisture_optimal = current_moisture_level >= optimal_moisture_level\nprint(is_moisture_optimal)\n\nTrue\n\n\n\nchance_rain_tonight = 10 # probability of rainfall as a percentage\n\nwater_plants = (current_moisture_level >= optimal_moisture_level) and (chance_rain_tonight < 50)\nprint(water_plants)\n\nTrue" + }, + { + "objectID": "basic_concepts/data_types.html#conversion-between-data-types", + "href": "basic_concepts/data_types.html#conversion-between-data-types", + "title": "14  Data types", + "section": "Conversion between data types", + "text": "Conversion between data types\nIn Python, converting between different data types, a process known as type casting, is a common and straightforward operation. You can convert data types using built-in functions like int(), float(), str(), and bool(). For instance, int() can change a floating-point number or a string into an integer, float() can turn an integer or string into a floating-point number, and str() can convert an integer or float into a string. These conversions are especially useful when you need to perform operations that require specific data types, such as mathematical calculations or text manipulation. However, it’s important to be mindful that attempting to convert incompatible types (like trying to turn a non-numeric string into a number) can lead to errors.\n\n# Integers to string\n\nint_num = 8\nprint(int_num)\nprint(type(int_num)) # Print data type before conversion\n\nint_str = str(int_num)\nprint(int_str)\nprint(type(int_str)) # Print resulting data type \n\n8\n<class 'int'>\n8\n<class 'str'>\n\n\n\n# Floats to string\n\nfloat_num = 3.1415\nprint(float_num)\nprint(type(float_num)) # Print data type before conversion\n\nfloat_str = str(float_num)\nprint(float_str)\nprint(type(float_str)) # Print resulting data type \n\n3.1415\n<class 'float'>\n3.1415\n<class 'str'>\n\n\n\n# Strings to integers/floats\n\nfloat_str = '3'\nfloat_num = float(float_str)\nprint(float_num)\nprint(type(float_num))\n\n# Check if string is numeric\nfloat_str.isnumeric()\n\n3.0\n<class 'float'>\n\n\nTrue\n\n\n\n# Floats to integers\nfloat_num = 4.9\nint_num = int(float_num)\n\nprint(int_num)\nprint(type(int_num))\n\n4\n<class 'int'>\n\n\nIn some cases Python will change the class according to the operation. For instance, the following code starts from two integers and results in a floating point.\n\nnumerator = 5\ndenominator = 2\nprint(type(numerator))\nprint(type(denominator))\n\nanswer = numerator / denominator # Two integers\nprint(answer)\nprint(type(answer)) # Result is a float\n\n<class 'int'>\n<class 'int'>\n2.5\n<class 'float'>" + }, + { + "objectID": "basic_concepts/data_structures.html#lists", + "href": "basic_concepts/data_structures.html#lists", + "title": "15  Data structures", + "section": "Lists", + "text": "Lists\nLists are versatile data structures defined by square brackets [ ] that ideal for storing sequences of elements, such as strings, numbers, or a mix of different data types. Lists are mutable, meaning that you can modify their content. Lists also support nesting, where a list can contain other lists. A key feature of lists is the ability to access elements through indexing (for single item) or slicing (for multiple items). While similar to arrays in other languages, like Matlab, it’s important to note that Python lists do not natively support element-wise operations, a functionality that is characteristic of NumPy arrays, a more advanced module that we will explore later.\n\n# List with same data type\nsoil_texture = [\"Sand\", \"Loam\", \"Silty clay\", \"Silt loam\", \"Silt\"] # Strings (soil textural classes)\nmean_sand = [92, 40, 5, 20, 5] # Integers (percent sand for each soil textural class)\n\nprint(soil_texture)\nprint(type(soil_texture)) # Print type of data structure\n \n# List with mixed data types (strings, floats, and an entire dictionary)\n# Sample ID, soil texture, pH value, and multiple nutrient concentration in ppm\nsoil_sample = [\"Sample_001\", \"Loam\", 6.5, {\"N\": 20, \"P\": 15, \"K\": 5}] \n\n['Sand', 'Loam', 'Silty clay', 'Silt loam', 'Silt']\n<class 'list'>\n\n\n\n# Indexing a list\nprint(soil_texture[0]) # Accesses the first item\nprint(soil_sample[2])\n\nSand\n6.5\n\n\n\n# Slicing a list\nprint(soil_texture[2:4])\nprint(soil_texture[2:])\nprint(soil_texture[:3])\n\n['Silty clay', 'Silt loam']\n['Silty clay', 'Silt loam', 'Silt']\n['Sand', 'Loam', 'Silty clay']\n\n\n\n# Find the length of a list\nprint(len(soil_texture)) # Returns the number of items\n\n5\n\n\n\n\n\n\n\n\nNote\n\n\n\nCan you guess how many items are in the soil_sample list? Use Python to check your answer!\n\n\n\n# Append elements to a list\nsoil_texture.append(\"Clay\") # Adds 'Barley' to the list 'crops'\nprint(soil_texture)\n\n['Sand', 'Loam', 'Silty clay', 'Silt loam', 'Silt', 'Clay']\n\n\n\n# Append multiple elements\nsoil_texture.extend([\"Loamy sand\", \"Sandy loam\"])\nprint(soil_texture)\n\n['Sand', 'Loam', 'Silty clay', 'Silt loam', 'Silt', 'Clay', 'Loamy sand', 'Sandy loam']\n\n\n\n\n\n\n\n\nNote\n\n\n\nAppending multiple items using the append() method will result in nested lists, while using the extend() method will results in merged lists. Give it a try and see if you can observe the difference.\n\n\n\n# Remove list element\nsoil_texture.remove(\"Clay\")\nprint(soil_texture)\n\n['Sand', 'Loam', 'Silty clay', 'Silt loam', 'Silt', 'Loamy sand', 'Sandy loam']\n\n\n\n# Insert an item at a specified position or index\nsoil_texture.insert(2, \"Clay\") # Inserts 'Clay' back again, but at index 2\nprint(soil_texture)\n\n['Sand', 'Loam', 'Clay', 'Silty clay', 'Silt loam', 'Silt', 'Loamy sand', 'Sandy loam']\n\n\n\n# Remove element based on index\nsoil_texture.pop(4)\nprint(soil_texture)\n\n['Sand', 'Loam', 'Clay', 'Silty clay', 'Silt', 'Loamy sand', 'Sandy loam']\n\n\n\n# An alternative method to delete one or more elements of the list.\ndel soil_texture[1:3]\nprint(soil_texture)\n\n['Sand', 'Silty clay', 'Silt', 'Loamy sand', 'Sandy loam']" + }, + { + "objectID": "basic_concepts/data_structures.html#tuples", + "href": "basic_concepts/data_structures.html#tuples", + "title": "15  Data structures", + "section": "Tuples", + "text": "Tuples\nTuples are an efficient data structure defined by parentheses ( ), and are especially useful for storing fixed sets of elements like coordinates in a two-dimensional plane (e.g., point(x, y)) or triplets of color values in the RGB color space (e.g., (r, g, b)). While tuples can be nested within lists and support operations similar to lists, like indexing and slicing, the main difference is that tuples are immutable. Once a tuple is created, its content cannot be changed. This makes tuples particularly valuable for storing critical information that must remain constant in your code.\n\n# Geographic coordinates \nmauna_loa = (19.536111, -155.576111, 3397) # Mauna Load Observatory in Hawaii, USA\nkonza_prairie = (39.106704, -96.608968, 320) # Konza Prairie in Kansas, USA\n\nlocations = [mauna_loa, konza_prairie]\nprint(locations)\n\n[(19.536111, -155.576111, 3397), (39.106704, -96.608968, 320)]\n\n\n\n# A list of tuples\ncolors = [(0,0,0), (255,255,255), (0,255,0)] # Each tuple refers to black, white, and green.\nprint(colors)\nprint(type(colors[0]))\n\n[(0, 0, 0), (255, 255, 255), (0, 255, 0)]\n<class 'tuple'>\n\n\n\n\n\n\n\n\nNote\n\n\n\nWhat happens if we want to change the first element of the third tuple from 0 to 255? Hint: colors[2][0] = 255" + }, + { + "objectID": "basic_concepts/data_structures.html#dictionaries", + "href": "basic_concepts/data_structures.html#dictionaries", + "title": "15  Data structures", + "section": "Dictionaries", + "text": "Dictionaries\nDictionaries are a highly versatile and popular data structure that have the peculiar ability to store and retrieve data using key-value pairs defined within curly braces { } or using the dict() function. This means that you can access, add, or modify data using unique keys, making dictionaries incredibly efficient for organizing and handling data using named references.\nDictionaries are particularly useful in situations where data doesn’t fit neatly into a matrix or table format and has multiple attributes, such as weather data, where you might store various weather parameters (temperature, humidity, wind speed) using descriptive keys. Unlike lists or tuples, dictionaries aren’t ordered by nature, but they excel in scenarios where each piece of data needs to be associated with a specific identifier. This structure provides a straightforward and intuitive way to manage complex, unstructured data.\n\n# Weather data is often stored in dictionary or dictionary-like data structures.\nD = {'city':'Manhattan',\n 'state':'Kansas',\n 'coords': (39.208722, -96.592248, 350),\n 'data': [{'date' : '20220101', \n 'precipitation' : {'value':12.5, 'unit':'mm', 'instrument':'TE525'},\n 'air_temperature' : {'value':5.6, 'units':'Celsius', 'instrument':'ATMOS14'}\n },\n {'date' : '20220102', \n 'precipitation' : {'value':0, 'unit':'mm', 'instrument':'TE525'},\n 'air_temperature' : {'value':1.3, 'units':'Celsius', 'instrument':'ATMOS14'}\n }]\n }\n\nprint(D)\nprint(type(D))\n\n{'city': 'Manhattan', 'state': 'Kansas', 'coords': (39.208722, -96.592248, 350), 'data': [{'date': '20220101', 'precipitation': {'value': 12.5, 'unit': 'mm', 'instrument': 'TE525'}, 'air_temperature': {'value': 5.6, 'units': 'Celsius', 'instrument': 'ATMOS14'}}, {'date': '20220102', 'precipitation': {'value': 0, 'unit': 'mm', 'instrument': 'TE525'}, 'air_temperature': {'value': 1.3, 'units': 'Celsius', 'instrument': 'ATMOS14'}}]}\n<class 'dict'>\n\n\nThe example above has several interesting features: - The city and state names are ordinary strings - The geographic coordinates (latitude, longitude, and elevation) are grouped using a tuple. - Weather data for each day is a list of dictionaries - In a single dictionary we have observations for a given timestamp together with the associated metadata including units, sensors, and location. Personally I think that dictionaries are ideal data structures in the context of reproducible science.\n\n\n\n\n\n\nNote\n\n\n\nThe structure of the dictionary above depends on programmer preferences. For instance, rather than grouping all three coordinates into a tuple, a different programmer may prefer to store the values under individual name:value pairs, such as: latitude : 39.208722, longitude : -96.592248, and altitude : 350)" + }, + { + "objectID": "basic_concepts/data_structures.html#sets", + "href": "basic_concepts/data_structures.html#sets", + "title": "15  Data structures", + "section": "Sets", + "text": "Sets\nSets are a unique and somewhat less commonly used data structure compared to lists, tuples, and dictionaries. Sets are defined with curly braces { } (without defining key-value pairs) or the set() function and are similar to mathematical sets, meaning they store unordered collections of unique items. In other words, Sets don’t allow for duplicate items, items cannot be changed (although items can be added and removed), and items are not indexed. This makes sets ideal for operations like determining membership, eliminating duplicates, and performing mathematical set operations such as unions, intersections, and differences. In scenarios like database querying or data analysis where you need to compare different datasets, sets can be used to find common elements (intersection), all elements (union), or differences between datasets.\n\n# Union operation\nfield1_weeds = set([\"Dandelion\", \"Crabgrass\", \"Thistle\", \"Dandelion\"])\nfield2_weeds = set([\"Thistle\", \"Crabgrass\", \"Foxtail\"])\nunique_weeds = field1_weeds.union(field2_weeds)\nprint(unique_weeds)\n\n{'Crabgrass', 'Foxtail', 'Thistle', 'Dandelion'}\n\n\n\n# Intersection operation\ncommon_weeds = field1_weeds.intersection(field2_weeds)\nprint(common_weeds)\n\n{'Crabgrass', 'Thistle'}\n\n\n\n# Difference operation\ndifferent_weeds_in_field1 = field1_weeds.difference(field2_weeds)\nprint(different_weeds_in_field1)\n\ndifferent_weeds_in_field2 = field2_weeds.difference(field1_weeds)\nprint(different_weeds_in_field2)\n\n{'Dandelion'}\n{'Foxtail'}\n\n\n\n# We can also chain more variables if needed\nfield3_weeds = set([\"Pigweed\", \"Clover\"])\nfield1_weeds.union(field2_weeds).union(field3_weeds)\n\n{'Clover', 'Crabgrass', 'Dandelion', 'Foxtail', 'Pigweed', 'Thistle'}\n\n\n\n\n\n\n\n\nNote\n\n\n\nFor this particular example, you could leverage a set data structure to easily compare field notes from multiple agronomists collecting information across farmer fields in a given region and quickly determine dominant weed species." + }, + { + "objectID": "basic_concepts/data_structures.html#practice", + "href": "basic_concepts/data_structures.html#practice", + "title": "15  Data structures", + "section": "Practice", + "text": "Practice\n\nCreate a list with the scientific names of three common grasses in the US Great Plains: big bluestem, switchgrass, indian grass, and little bluestem.\nUsing a periodic table, store in a dictionary the name, symbol, atomic mass, melting point, and boiling point of oxygen, nitrogen, phosphorus, and hydrogen. Then, write two separate python statements to retrieve the boiling point of oxygen and hydrogen. Combined, these two atoms can form water, which has a boiling point of 100 degrees Celsius. How does this value compare to the boiling point of the individual elements?\nWithout editing the dictionary that you created in the previous point, append the properties for a new element: carbon.\nCreate a list of tuples encoding the latitude, longitude, and altitude of three national parks of your choice." + }, + { + "objectID": "basic_concepts/dates_and_times.html#basic-datetime-syntax", + "href": "basic_concepts/dates_and_times.html#basic-datetime-syntax", + "title": "16  Working with dates and times", + "section": "Basic datetime Syntax", + "text": "Basic datetime Syntax\n\n# Current date and time\nnow = datetime.now()\nprint(type(now)) # show data type\nprint(\"Current Date and Time:\", now)\n\n# Specific date and time\nplanting_date = datetime(2022, 4, 15, 8, 30)\nprint(\"Planting Date and Time:\", planting_date.strftime('%Y-%m-%d %H:%M:%S'))\n\n# Parsing String to Datetime\nharvest_date = datetime.strptime(\"2022-09-15 14:00:00\", '%Y-%m-%d %H:%M:%S')\nprint(\"Harvest Date:\", harvest_date)\n\n# We can also add custom datetime format in f-strings\nprint(f\"Harvest Date: {harvest_date:%A, %B %d, %Y}\")\n\n<class 'datetime.datetime'>\nCurrent Date and Time: 2024-01-12 16:56:03.399852\nPlanting Date and Time: 2022-04-15 08:30:00\nHarvest Date: 2022-09-15 14:00:00\nHarvest Date: Thursday, September 15, 2022" + }, + { + "objectID": "basic_concepts/dates_and_times.html#working-with-timedelta", + "href": "basic_concepts/dates_and_times.html#working-with-timedelta", + "title": "16  Working with dates and times", + "section": "Working with timedelta", + "text": "Working with timedelta\n\n# Difference between two dates\nduration = harvest_date - planting_date\nprint(\"Days Between Planting and Harvest:\", duration.days)\n\n# Total seconds of the duration\nprint(\"Total Seconds:\", duration.total_seconds())\n\n# Use total seconds to compute number of hours\nprint(\"Total hours:\", round(duration.total_seconds()/3600), 'hours') # 3600 seconds per hour\n\n# Adding ten days\nemergence_date = planting_date + timedelta(days=10)\nprint(\"Crop emergence was on:\", emergence_date)\n\nDays Between Planting and Harvest: 153\nTotal Seconds: 13239000.0\nTotal hours: 3678 hours\nCrop emergence was on: 2022-04-25 08:30:00" + }, + { + "objectID": "basic_concepts/dates_and_times.html#using-pandas-for-datetime-operations", + "href": "basic_concepts/dates_and_times.html#using-pandas-for-datetime-operations", + "title": "16  Working with dates and times", + "section": "Using Pandas for datetime operations", + "text": "Using Pandas for datetime operations\n\n# Import module\nimport pandas as pd\n\n\n# Create a DataFrame with dates\ndf = pd.DataFrame({\n \"planting_dates\": [\"2020-04-15\", \"2021-04-25\", \"2022-04-7\"],\n \"harvest_dates\": [\"2020-09-15\", \"2021-10-1\", \"2022-09-25\"]\n})\n\n# Convert string to datetime in Pandas\ndf['planting_dates'] = pd.to_datetime(df['planting_dates'], format='%Y-%m-%d')\ndf['harvest_dates'] = pd.to_datetime(df['harvest_dates'], format='%Y-%m-%d')\n\n# Add a timedelta column\ndf['growing_season_length'] = df['harvest_dates'] - df['planting_dates']\n\n# Display dataframe\ndf.head()\n\n\n\n\n\n\n\n\nplanting_dates\nharvest_dates\ngrowing_season_length\n\n\n\n\n0\n2020-04-15\n2020-09-15\n153 days\n\n\n1\n2021-04-25\n2021-10-01\n159 days\n\n\n2\n2022-04-07\n2022-09-25\n171 days" + }, + { + "objectID": "basic_concepts/indexing_and_slicing.html#syntax-for-indexing-and-slicing-one-dimensional-arrays", + "href": "basic_concepts/indexing_and_slicing.html#syntax-for-indexing-and-slicing-one-dimensional-arrays", + "title": "17  Indexing and slicing", + "section": "Syntax for indexing and slicing one-dimensional arrays", + "text": "Syntax for indexing and slicing one-dimensional arrays\n# Indexing a one-dimensional array\nelement = array[index]\n\n# Slicing a one-dimensional array\nsub_array = array[start_index:end_index:step]\nOmitting start_index (e.g., array[:end_index]) slices from the beginning to end_index.\nOmitting end_index (e.g., array[start_index:]) slices from start_index to the end of the array.\n\n# Generate intengers from 0 to the specified number (non-inclusive)\nnumbers = list(range(10)) \nprint(numbers)\n\n# Find the first element of the list (indexing operation)\nprint(numbers[0])\n\n# First and second element\nprint(numbers[0:2]) \n\n# Print last three elements\nprint(numbers[-3:]) \n\n# All elements (from 0 and on)\nprint(numbers[0:])\n\n# Every other element (specifying the total number of element)\nprint(numbers[0:10:2]) \n\n# Every other element (without specifying the total number of elements)\nprint(numbers[0:-1:2])\n\n# Print the first 3 elements\nprint(numbers[:3])\n\n# Slice from the 4th to the next-to-last element\nprint(numbers[4:-1]) \n\n# Print the last item of the list\nprint(numbers[-1])\nprint(numbers[len(numbers)-1])\n\n[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n0\n[0, 1]\n[7, 8, 9]\n[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n[0, 2, 4, 6, 8]\n[0, 2, 4, 6, 8]\n[0, 1, 2]\n[4, 5, 6, 7, 8]\n9\n9" + }, + { + "objectID": "basic_concepts/indexing_and_slicing.html#syntax-for-indexing-and-slicing-two-dimensional-arrays", + "href": "basic_concepts/indexing_and_slicing.html#syntax-for-indexing-and-slicing-two-dimensional-arrays", + "title": "17  Indexing and slicing", + "section": "Syntax for indexing and slicing two-dimensional arrays", + "text": "Syntax for indexing and slicing two-dimensional arrays\n# Indexing a two-dimensional array\nelement = array[row_index][column_index]\n\n# Slicing a two-dimensional array (with step)\nsub_array = array[row_start:row_end:row_step, column_start:column_end:column_step]\nOmitting row_start (e.g., array[:row_end, :]) slices from the beginning to row_end in all columns. Here “all columns” is represented by the : operator. Similarly you can use : to represent all rows.\nOmitting row_end (e.g., array[row_start:, :]) slices from row_start to the end in all columns.\n\nimport numpy as np\n\n# Indexing and slicing a two-dimensional array\nnp.random.seed(0)\nM = np.random.randint(0,10,[5,5])\nprint(M)\n\n# Write five python commands to obtain:\n# top row\n# bottom row\n# right-most column\n# left-most column\n# upper-right 3x3 matrix\n\n[[5 0 3 3 7]\n [9 3 5 2 4]\n [7 6 8 8 1]\n [6 7 7 8 1]\n [5 9 8 9 4]]\n\n\n\n# Solutions\n\n# Top row\nprint('Top row')\nprint(M[0,:]) # Preferred\nprint(M[:1][0])\nprint(M[0])\nprint('')\n\n# Bottom row\nprint('Bottom row')\nprint(M[-1,:]) # Preferred\nprint(M[-1])\nprint(M[4,:]) # Requires knowing size of array in advance\nprint('')\n\n# Right-most column\nprint('Right-most column')\nprint(M[:,-1])\nprint('')\n\n# Left-most column\nprint('Left-most column')\nprint(M[:,0])\nprint('')\n\n# Upper-right 3x3 matrix\nprint('Upper 3x3 matrix')\nprint(M[0:3,M.shape[1]-3:M.shape[1]]) # More versatile\nprint(M[0:3,2:M.shape[1]])\n\nTop row\n[5 0 3 3 7]\n[5 0 3 3 7]\n[5 0 3 3 7]\n\nBottom row\n[5 9 8 9 4]\n[5 9 8 9 4]\n[5 9 8 9 4]\n\nRight-most column\n[7 4 1 1 4]\n\nLeft-most column\n[5 9 7 6 5]\n\nUpper 3x3 matrix\n[[3 3 7]\n [5 2 4]\n [8 8 1]]\n[[3 3 7]\n [5 2 4]\n [8 8 1]]" + }, + { + "objectID": "basic_concepts/indexing_and_slicing.html#references", + "href": "basic_concepts/indexing_and_slicing.html#references", + "title": "17  Indexing and slicing", + "section": "References", + "text": "References\nSource: https://stackoverflow.com/questions/509211/understanding-slice-notation" + }, + { + "objectID": "basic_concepts/if_statement.html#syntax", + "href": "basic_concepts/if_statement.html#syntax", + "title": "18  If statements", + "section": "Syntax", + "text": "Syntax\nif condition:\n # Code block executes if condition is true\nelif another_condition:\n # Code block executes if another condition is true\nelse:\n # Code block executes if none of the above conditions are true" + }, + { + "objectID": "basic_concepts/if_statement.html#example-1-soil-ph", + "href": "basic_concepts/if_statement.html#example-1-soil-ph", + "title": "18  If statements", + "section": "Example 1: Soil pH", + "text": "Example 1: Soil pH\nSoil pH is a measure of soil acidity or alkalinity. The pH scale ranges from 0 to 14, with a pH of 7 considered neutral. Acidic soils have pH values below 7, indicating an abundance of hydrogen ions (H+), while alkaline soils have pH values above 7, indicating an excess of hydroxide ions (OH-). Soil pH plays a vital role in nutrient availability to plants, microbial activity, and soil structure.\n\npH = 7 # Soil pH\n\nif pH >= 0 and pH < 7:\n soil_class = 'Acidic'\nelif pH == 7:\n soil_class = 'Neutral'\nelif pH > 7 and pH <= 14:\n soil_class = 'Alkaline'\nelse:\n soil_class = 'pH value out of range.'\n\nprint(soil_class)\n\nNeutral" + }, + { + "objectID": "basic_concepts/if_statement.html#example-2-saline-sodic-and-soline-sodic-soils", + "href": "basic_concepts/if_statement.html#example-2-saline-sodic-and-soline-sodic-soils", + "title": "18  If statements", + "section": "Example 2: Saline, sodic, and soline-sodic soils", + "text": "Example 2: Saline, sodic, and soline-sodic soils\nFor instance, in soil science we can use if statements to categorize soils into saline, saline-sodic, or sodic based on electrical conductivity (EC) and the Sodium Adsorption Ratio (SAR). Sometimes soil pH is also a component of this classification, but to keep it simple we will only use EC and SAR for this example. We can write a program that uses if statements to classify salt-affected soils following this widely-used table:\n\n\n\nSoil Type\nEC\nSAR\n\n\n\n\nNormal\n< 4.0 dS/m\n< 13\n\n\nSaline\n≥ 4.0 dS/m\n< 13\n\n\nSodic\n< 4.0 dS/m\n≥ 13\n\n\nSaline-Sodic\n≥ 4.0 dS/m\n≥ 13\n\n\n\nHere is a great fact sheet where you can learn more about saline, sodic, and saline-sodic soils\n\nec = 3 # electrical conductivity in dS/m\nsar = 16 # sodium adsorption ratio (dimensionless)\n\n# Determine the category of soil\nif (ec >= 4.0) and (sar < 13):\n classification = \"Saline\"\n\nelif (ec < 4.0) and (sar >= 13):\n classification = \"Sodic\"\n\nelif (ec >= 4.0) and (sar >= 13):\n classification = \"Saline-Sodic\"\n\nelse:\n classification = \"Normal\"\n \nprint(f'Soil is {classification}')\n \n\nSoil is Sodic" + }, + { + "objectID": "basic_concepts/if_statement.html#example-3-climate-classification-based-on-aridity-index", + "href": "basic_concepts/if_statement.html#example-3-climate-classification-based-on-aridity-index", + "title": "18  If statements", + "section": "Example 3: Climate classification based on aridity index", + "text": "Example 3: Climate classification based on aridity index\nTo understand global climate variations, one useful metric is the Aridity Index (AI), which compares annual precipitation to atmospheric demand. Essentially, a lower AI indicates a drier region:\n AI = \\frac{P}{PET} \nwhere P is the annual precipitation in mm and PET is the annual cummulative potential evapotranspiration in mm.\nOver time, the definition of AI has evolved, leading to various classifications in the literature. Below is a simplified summary of these classifications:\n\n\n\nClimate class\nValue\n\n\n\n\nHyper-arid\n0.03 < AI\n\n\nArid\n0.03 < AI ≤ 0.20\n\n\nSemi-arid\n0.20 < AI ≤ 0.50\n\n\nDry sub-humid\n0.50 < AI ≤ 0.65\n\n\nSub-humid\n0.65 < AI ≤ 0.75\n\n\nHumid\nAI > 0.75\n\n\n\n\n# Define annual precipitation and atmospheric demand for a location\nP = 1200 # mm per year\nPET = 1800 # mm per year\n\n# Compute Aridity Inde\nAI = P/PET \n\n# Find climate class\nif AI <= 0.03:\n climate_class = 'Arid'\n \nelif AI > 0.03 and AI <= 0.2:\n climate_class = 'Arid'\n\nelif AI > 0.2 and AI <= 0.5:\n climate_class = 'Semi-arid'\n\nelif AI > 0.5 and AI <= 0.65:\n climate_class = 'Dry sub-humid'\n \nelif AI > 0.65 and AI <= 0.75:\n climate_class = 'Sub-humid'\n \nelse:\n climate_class = 'Humid'\n \nprint('Climate classification for this location is:',climate_class,'(AI='+str(round(AI,2))+')')\n\nClimate classification for this location is: Sub-humid (AI=0.67)" + }, + { + "objectID": "basic_concepts/if_statement.html#comparative-anatomy-of-if-statements", + "href": "basic_concepts/if_statement.html#comparative-anatomy-of-if-statements", + "title": "18  If statements", + "section": "Comparative anatomy of If statements", + "text": "Comparative anatomy of If statements\n\nPython\npH = 7 # Soil pH\n\nif pH >= 0 and pH < 7:\n soil_class = 'Acidic'\nelif pH == 7:\n soil_class = 'Neutral'\nelif pH > 7 and pH <= 14:\n soil_class = 'Alkaline'\nelse:\n soil_class = 'pH value out of range.'\n\nprint(soil_class)\n\n\nMatlab\npH = 7; % Soil pH\n\nif pH >= 0 && pH < 7\n soil_class = 'Acidic';\nelseif pH == 7\n soil_class = 'Neutral';\nelseif pH > 7 && pH <= 14\n soil_class = 'Alkaline';\nelse\n soil_class = 'pH value out of range.';\nend\n\ndisp(soil_class)\n\n\nJulia\npH = 7 # Soil pH\n\nif 0 <= pH < 7\n soil_class = \"Acidic\"\nelseif pH == 7\n soil_class = \"Neutral\"\nelseif 7 < pH <= 14\n soil_class = \"Alkaline\"\nelse\n soil_class = \"pH value out of range.\"\nend\n\nprintln(soil_class)\n\n\nR\npH <- 7 # Soil pH\n\nif (pH >= 0 & pH < 7) {\n soil_class <- \"Acidic\"\n} else if (pH == 7) {\n soil_class <- \"Neutral\"\n} else if (pH > 7 & pH <= 14) {\n soil_class <- \"Alkaline\"\n} else {\n soil_class <- \"pH value out of range.\"\n}\n\nprint(soil_class)\n\n\nJavaScript\nlet pH = 7; // Soil pH\n\nlet soil_class;\nif (pH >= 0 && pH < 7) {\n soil_class = 'Acidic';\n} else if (pH === 7) {\n soil_class = 'Neutral';\n} else if (pH > 7 && pH <= 14) {\n soil_class = 'Alkaline';\n} else {\n soil_class = 'pH value out of range.';\n}\n\nconsole.log(soil_class);\n\n\nCommonalities among programming languages:\n\nAll languages use a conditional if keyword to start the statement.\nThey employ logical conditions (e.g., <, ==, <=) to evaluate true or false.\nThe use of else if or its equivalent for additional conditions.\nThe presence of else to handle cases that don’t meet any if or else if conditions.\nBlock of code under each condition is indented or enclosed (e.g., {} in JavaScript, end in Matlab and Julia)." + }, + { + "objectID": "basic_concepts/if_statement.html#references", + "href": "basic_concepts/if_statement.html#references", + "title": "18  If statements", + "section": "References", + "text": "References\nHavlin, J. L., Tisdale, S. L., Nelson, W. L., & Beaton, J. D. (2016). Soil fertility and fertilizers. Pearson Education India.\nSpinoni, J., Vogt, J., Naumann, G., Carrao, H. and Barbosa, P., 2015. Towards identifying areas at climatological risk of desertification using the Köppen–Geiger classification and FAO aridity index. International Journal of Climatology, 35(9), pp.2210-2222.\nZhang, H. 2017. Interpreting Soil Salinity Analyses. L-297. Oklahoma Cooperative Extension Service. Link\nZomer, R. J., Xu, J., & Trabucco, A. (2022). Version 3 of the global aridity index and potential evapotranspiration database. Scientific Data, 9(1), 409." + }, + { + "objectID": "basic_concepts/functions.html#syntax", + "href": "basic_concepts/functions.html#syntax", + "title": "19  Functions", + "section": "Syntax", + "text": "Syntax\nThis is the main syntax to define your own functions:\ndef function_name(parameters, par_opt=par_value):\n # Code block\n return result\nLet’s look at a few examples to see this two-step process in action." + }, + { + "objectID": "basic_concepts/functions.html#example-function-compute-vapor-pressure-deficit", + "href": "basic_concepts/functions.html#example-function-compute-vapor-pressure-deficit", + "title": "19  Functions", + "section": "Example function: Compute vapor pressure deficit", + "text": "Example function: Compute vapor pressure deficit\nThe vapor pressure deficit (VPD) represents the “thirst” of the atmosphere and is computed as the difference between the saturation vapor pressure and the actual vapor pressure. The saturation vapor pressure can be accurately approximated as a function of air temperature using the empirical Tetens equation. Here is the set equations to compute VPD:\nSaturation vapor pressure: e_{sat} = 0.611 \\; exp\\Bigg(\\frac{17.502 \\ T} {T + 240.97}\\Bigg)\nActual vapor pressure: e_{act} = e_{sat} \\frac{RH}{100}\nVapor pressure deficit: VPD = e_{sat} - e_{act}\nVariables\ne_{sat} is the saturation vapor pressure deficit (kPa)\ne_{act} is the actual vapor pressure (kPa)\nVPD is the vapor pressure deficit (kPa)\nT is air temperature (^\\circC)\nRH is relative humidity (%)\n\nDefine function\nIn the following example we will focus on the main building blocks of a function, but we will ignore error handling and checks to ensure that inputs have the proper data type. For more details on how to properly handle errors and ensure inputs have the correct data type see the error handling tutorial.\n\n# Import necessary modules\nimport numpy as np\n\n\n# Define function\n\ndef compute_vpd(T, RH, unit='kPa'):\n \"\"\"\n Function that computes the air vapor pressure deficit (vpd).\n\n Parameters:\n T (integer, float): Air temperature in degrees Celsius.\n RH (integer, float): Air relative humidity in percentage.\n unit (string): Unit of the output vpd value. \n One of the following: kPa (default), bars, psi\n\n Returns:\n float: Vapor pressure deficit in kiloPascals (kPa).\n \n Authors:\n Andres Patrignani\n\n Date created:\n 6 January 2024\n \n Reference:\n Campbell, G. S., & Norman, J. M. (2000).\n An introduction to environmental biophysics. Springer Science & Business Media.\n \"\"\"\n\n # Compute saturation vapor pressure\n e_sat = 0.611 * np.exp((17.502*T) / (T + 240.97)) # kPa\n\n # Compute actual vapor pressure\n e_act = e_sat * RH/100 # kPa\n\n # Compute vapor pressure deficit\n vpd = e_sat - e_act # kPa\n \n # Change units if necessary\n if unit == 'bars':\n vpd *= 0.01 # Same as vpd = vpd * 0.01\n \n elif unit == 'psi':\n vpd *= 0.1450377377 # Convert to pounds per square inch (psi)\n\n return vpd\n\n\n\n\n\n\n\nSyntax note\n\n\n\nDid you notice the expression vpd *= 0.01? This is a compact way in Python to do vpd = vpd * 0.01. You can also use it with other operators, like += for adding or -= for subtracting values from a variable.\n\n\n\n\nDescription of function components\n\nFunction Definition: def compute_vpd(T, RH, unit='kPa'): This line defines the function with the name compute_vpd, which takes two parameters, T for temperature and RH for relative humidity. The function also includes an optional argument unit= that has a default value of kPa.\nDocstring:\n\"\"\"\nFunction that computes the air vapor pressure deficit (vpd).\n\nParameters:\nT (integer, float): Air temperature in degrees Celsius.\nRH (integer, float): Air relative humidity in percentage.\nunit (string): Unit of the output vpd value. \n One of the following: kPa (default), bars, psi\n\nReturns:\nfloat: Vapor pressure deficit in kiloPascals (kPa).\n\nAuthors:\nAndres Patrignani\n\nDate created:\n6 January 2024\n\nReference:\nCampbell, G. S., & Norman, J. M. (2000).\nAn introduction to environmental biophysics. Springer Science & Business Media.\n\"\"\"\nThe triple-quoted string right after the function definition is the docstring. It provides a brief description of the function, its parameters, their data types, and what the function returns.\nSaturation Vapor Pressure Calculation:\ne_sat = 0.611 * np.exp((17.502*T) / (T + 240.97)) # kPa\nThis line of code calculates the saturation vapor pressure (e_sat) using air temperature T. It’s a mathematical expression that uses the exp function from the NumPy library (np), which should be imported at the beginning of the script.\nActual Vapor Pressure Calculation:\ne_act = e_sat * RH/100 # kPa\nThis line calculates the actual vapor pressure (e_act) based on the saturation vapor pressure and the relative humidity RH of air.\nVapor Pressure Deficit Calculation:\nvpd = e_sat - e_act # kPa\nHere, the vapor pressure deficit (vpd) is computed by subtracting the actual vapor pressure from the saturation vapor pressure.\nUnit conversion:\n# Change units if necessary\nif unit == 'bars':\n vpd = vpd * 0.01\n\nelif unit == 'psi':\n vpd = vpd * 0.1450377377 # Convert to pounds per square inch (psi)\nIn this step we change the units of the resulting vpd before returning the output. Note that since the value of vpd using the equations in the function is already in kPa, so there is no need to handle this scenario in the if statement.\nReturn Statement:\nreturn vpd\nThe return statement sends back the result of the function (vpd) to wherever the function was called.\n\n\n\n\n\n\n\nNote\n\n\n\nIn Python functions, you can use optional parameters with default values for flexibility, placing them after mandatory parameters in the function’s definition.\n\n\n\n\nCall function\nHaving named our function and defined its inputs, we can now invoke the function without duplicating the code.\n\n# Define input variables\nT = 25 # degrees Celsius\nRH = 75 # percentage\n\n# Call the function (without using the optional argument)\nvpd = compute_vpd(T, RH)\n\n# Display variable value\nprint(f'The vapor pressure deficit is {vpd:.2f} kPa')\n\nThe vapor pressure deficit is 0.79 kPa\n\n\n\n# Call the function using the optional argument to specify the unit in `bars`\nvpd = compute_vpd(T, RH, unit='bars')\n\n# Display variable value\nprint(f'The vapor pressure deficit is {vpd:.3f} bars')\n\nThe vapor pressure deficit is 0.008 bars\n\n\n\n# Call the function using the optional argument to specify the unit in `psi`\nvpd = compute_vpd(T, RH, unit='psi')\n\n# Display variable value\nprint(f'The vapor pressure deficit is {vpd:.3f} psi')\n\nThe vapor pressure deficit is 0.115 psi\n\n\n\n\n\n\n\n\nImportant\n\n\n\nIn Python, the sequence in which you pass input arguments into a function is critical because the function expects them in the order they were defined. If you call compute_vpd with the inputs in the wrong order, like compute_vpd(RH, T), the function will still execute, but it will use relative humidity (RH) as temperature and temperature (T) as humidity, leading to incorrect results. To ensure accuracy, you must match the order to the function’s definition: compute_vpd(T, RH).\n\n\n\n\nEvaluate function performance\nCode performance in terms of execution time directly impacts the data analysis and visualization experience. The perf_counter() method within the time module provides a high-resolution timer that can be used to track the execution time of your code, offering a precise measure of performance. By recording the time immediately before and after a block of code runs, and then calculating the difference, perf_counter() helps you understand how long your code takes to execute. This is particularly useful for optimizing code and identifying bottlenecks in your Python programs. However, it is important to balance performance with the principle that premature optimization in the early stages of a project is often counterproductive. Optimization should come at a later stage when the code is correct and its performance bottlenecks are clearly identified.\n\n# Import time module\nimport time\n\n# Get initial time\ntic = time.perf_counter() \n\nvpd = compute_vpd(T, RH, unit='bars')\n\n# Get final time\ntoc = time.perf_counter() \n\n# Compute elapsed time\nelapsed_time = toc - tic\nprint(\"Elapsed time:\", elapsed_time, \"seconds\")\n\nElapsed time: 7.727195043116808e-05 seconds\n\n\n\n\nAccess function help (the docstring)\n\ncompute_vpd?\n\n\nSignature: compute_vpd(T, RH, unit='kPa')\nDocstring:\nFunction that computes the air vapor pressure deficit (vpd).\nParameters:\nT (integer, float): Air temperature in degrees Celsius.\nRH (integer, float): Air relative humidity in percentage.\nunit (string): Unit of the output vpd value. \n One of the following: kPa (default), bars, psi\nReturns:\nfloat: Vapor pressure deficit in kiloPascals (kPa).\nAuthors:\nAndres Patrignani\nDate created:\n6 January 2024\nReference:\nCampbell, G. S., & Norman, J. M. (2000).\nAn introduction to environmental biophysics. Springer Science & Business Media.\nFile: /var/folders/w1/cgh8d8y962g9c6p4_dxgbn2jh5jy11/T/ipykernel_40431/2839097177.py\nType: function" + }, + { + "objectID": "basic_concepts/functions.html#function-variable-scope", + "href": "basic_concepts/functions.html#function-variable-scope", + "title": "19  Functions", + "section": "Function variable scope", + "text": "Function variable scope\nOne aspect of Python functions that we did not cover is variable scope. In Python, variables defined inside a function are local to that function and can’t be accessed from outside of it, while variables defined outside of functions are global and can be accessed from anywhere in the script. It’s like having a conversation in a private room (function) versus a public area (global scope).\nTo prevent confusion, it’s best to follow good naming conventions for variables in your scripts. However, in extensive scripts with numerous functions—both written by you and imported from other modules—tracking every variable name can be challenging. This is where the local variable scope of Python functions comes to rescue, ensuring that variables within a function don’t interfere with those outside.\nBelow are a few examples to practice and consider.\n\nExample: Access a global variable from inside of a function\n\nvariable_outside = 1 # Accessible from anywhere in the script\n\ndef my_function():\n variable_inside = 2 # Only accessible within this function (it's not being used for anything in this case)\n print(variable_outside)\n \n# Invoke the function\nmy_function()\n\n1\n\n\n\n\nExample: Modify a global variable from inside of a function (will not work)\nThis example will not work, but I think it’s worth trying to learn. In this example, as soon as we request the interpreter to perform an operation on variable_outside, it searches in the local workspace of the function for a variable called variable_outside, but since this variable has not been defined WITHIN the function, then it throws an error. See the next example for a working solution.\n\nvariable_outside = 1 # Accessible from anywhere in the script\n\ndef my_function():\n variable_outside += 5\n print(variable_outside)\n \n# Invoke the function\nmy_function()\n\nUnboundLocalError: local variable 'variable_outside' referenced before assignment\n\n\n\n\nExample: Modify global variables inside of a function\nThe solution to the previous example is to explicitly tell Python to search for a global variable. The use of global variables is an advanced features and often times not recommended, since this practice tends to increase the complexity of the code.\n\nvariable_outside = 1 # Accessible from anywhere in the script\n\ndef my_function():\n \n # We tell Python to use the variable defined outside in the next line\n global variable_outside \n \n variable_outside += 5\n print(variable_outside)\n \n# Invoke the function\nmy_function() # The function changes the value of the variable_outside\n\n# Print the value of the variable\nprint(variable_outside) # Same value as before since we changed it inside the function\n\n6\n6\n\n\n\n\nExample: A global and a local variable with the same name\n\nvariable_outside = 1 # Accessible from anywhere in the script\n\ndef my_function():\n \n # A different variable with the same name. \n # Only available inside the function\n variable_outside = 1\n \n # We are changing the value in the previous line,\n # not the variable defined outside of the function\n variable_outside += 5\n \n print(variable_outside)\n \n# Invoke the function\nmy_function() # This prints the variable inside the function\n\n# This prints the variable we defined at the top, which remains unchanged\nprint(variable_outside)\n\n6\n1" + }, + { + "objectID": "basic_concepts/functions.html#python-utility-functions-zip-map-filter-and-reduce", + "href": "basic_concepts/functions.html#python-utility-functions-zip-map-filter-and-reduce", + "title": "19  Functions", + "section": "Python utility functions: zip, map, filter, and reduce", + "text": "Python utility functions: zip, map, filter, and reduce\n\nzip\nDescription: Aggregates elements from two or more iterables (like lists or tuples) and returns an iterator of tuples. Each tuple contains elements from the iterables, paired based on their order. For example, zip([1, 2], ['a', 'b']) would produce an iterator yielding (1, 'a') and (2, 'b'). If the iterables don’t have the same length, zip stops creating tuples when the shortest input iterable is exhausted. Use-case:This function is especially useful when you need to pair data elements from different sequences in a parallel manner.\n\n# Match content in two lists\nsampling_location = ['Manhattan','Colby','Tribune','Wichita','Lawrence']\nsoil_ph = [6.5, 6.1, 5.9, 7.0, 7.2]\n\nlist(zip(sampling_location, soil_ph))\n\n[('Manhattan', 6.5),\n ('Colby', 6.1),\n ('Tribune', 5.9),\n ('Wichita', 7.0),\n ('Lawrence', 7.2)]\n\n\n\n# The zip() function is useful to combine geographic information\n# Here are the geographic coordinates of five stations of the Kansas Mesonet\nlatitude = [39.12577,39.81409,39.41796, 37.99733, 38.84945]\nlongitude = [-96.63653, -97.67509, -97.13977, -100.81514, -99.34461]\naltitude = [324, 471, 388, 882, 618]\n\ncoords = list(zip(latitude, longitude, altitude))\nprint(coords)\n\n[(39.12577, -96.63653, 324), (39.81409, -97.67509, 471), (39.41796, -97.13977, 388), (37.99733, -100.81514, 882), (38.84945, -99.34461, 618)]\n\n\n\n\nmap\nDescription: Applies a given function to each item of an iterable (like a list) and returns a map object. Use-case: Transforming data elements into a collection.\n\ncelsius = [0,10,20,100]\nfahrenheit = list(map(lambda x: (x*9/5)+32, celsius))\nprint(fahrenheit)\n\n[32.0, 50.0, 68.0, 212.0]\n\n\n\n# Convert a DNA sequence into RNA\n# Remember that RNA contains uracil instead of thymine \ndna = 'ATTCGGGCAAATATGC'\nlookup = dict({\"A\":\"U\", \"T\":\"A\", \"C\":\"G\", \"G\":\"C\"})\nrna = list(map(lambda x: lookup[x], dna))\nprint(''.join(rna))\n\nUAAGCCCGUUUAUACG\n\n\n\n\nfilter\nDescription: Filters elements of an iterable based on a function that tests each element. Use-case: Selecting elements that meet specific criteria.\n\n# Get the occurrence of all adenine nucleotides\ndna = 'ATTCGGGCAAATATGC'\nlist(filter(lambda x: x == \"A\", dna))\n\n['A', 'A', 'A', 'A', 'A']\n\n\n\n# Find compacted soils\nbulk_densities = [1.01, 1.52, 1.84, 1.45, 1.32]\ncompacted_soils = list(filter(lambda x: x > 1.6, bulk_densities))\nprint(compacted_soils)\n\n[1.84]\n\n\n\n# Find hydrophobic soils based on regular function\ndef is_hydrophobic(contact_angle):\n \"\"\"\n Function that determines whether a soil is hydrophobic\n based on its contact angle.\n \"\"\"\n if contact_angle < 90:\n repel = False\n elif contact_angle >= 90 and contact_angle <= 180:\n repel = True\n \n return repel\n\ncontact_angles = [5,10,20,50,90,150]\nlist(filter(is_hydrophobic, contact_angles))\n\n[90, 150]\n\n\n\n\nreduce\nDescription: Applies a function cumulatively to the items of an iterable, reducing the iterable to a single value. Use-case: Aggregating data elements.\n\nfrom functools import reduce\n\n\n# Compute total yield\ncrop_yields = [1200, 1500, 1800, 2000]\ntotal_yield = reduce(lambda x, y: x + y, crop_yields)\nprint(total_yield)\n\n6500\n\n\n\n\n\n\n\n\nNote\n\n\n\nWhile the map, filter, and reduce functions are useful in standard Python, the functions are less critical when working with Pandas or NumPy, as these libraries already provide built-in, optimized methods for element-wise operations and data manipulation. Numpy typically surpasses the need for map, filter, or reduce in most scenarios." + }, + { + "objectID": "basic_concepts/functions.html#comparative-anatomy-of-functions", + "href": "basic_concepts/functions.html#comparative-anatomy-of-functions", + "title": "19  Functions", + "section": "Comparative anatomy of functions", + "text": "Comparative anatomy of functions\n\nPython\ndef hypotenuse(C1, C2):\n H = (C1**2 + C2**2)**0.5\n return H\n\nhypotenuse(3, 4)\n\n\nMatlab\nfunction H = hypotenuse(C1, C2)\n H = sqrt(C1^2 + C2^2);\nend\n\nhypotenuse(3, 4)\n\n\nJulia\nfunction hypotenuse(C1, C2)\n H = sqrt(C1^2 + C2^2)\n return H\nend\n\nhypotenuse(3, 4)\n\n\nR\nhypotenuse <- function(C1, C2) {\n H = sqrt(C1^2 + C2^2)\n return(H)\n}\n\nhypotenuse(3, 4)\n\n\nJavaScript\nfunction hypotenuse(C1, C2) {\n let H = Math.sqrt(C1**2 + C2**2);\n return H;\n}\n\nhypotenuse(3, 4);\n\n\nCommonalities among programming languages:\n\nAll languages use a keyword (like function or def) to define a function.\nThey specify function names and accept parameters within parentheses.\nThe body of the function is enclosed in a block (using braces {} like in JavaScript and R, indentation in the case of Python, or end in the case of Julia and Matlab).\nReturn statements are used to output the result of the function.\nFunctions are invoked by calling their name followed by arguments in parentheses." + }, + { + "objectID": "basic_concepts/functions.html#practice", + "href": "basic_concepts/functions.html#practice", + "title": "19  Functions", + "section": "Practice", + "text": "Practice\n\nCreate a function that computes the amount of lime required to increase an acidic soil pH. You can find examples in most soil fertility textbooks or extension fact sheets from multiple land-grant universities in the U.S.\nCreate a function that determines the amount of nitrogen required by a crop based on the amount of nitrates available at pre-planting, a yield goal for your region, and the amount of nitrogen required to produce the the yield goal.\nCreate a function to compute the amount of water storage in the soil profile from inputs of volumetric water content and soil depth.\nCreate a function that accepts latitude and longitude coordinates in decimal degrees and returns the latitude and longitude values in sexagesimal degrees. The function should accept Lat and Lon values as separate inputs e.g. fun(lat,lon) and must return a list of tuples with four components for each coordinate: degrees, minutes, seconds, and quadrant. The quadrant would be North/South for latitude and East/West for longitude. For instance: fun(19.536111, -155.576111) should result in [(19,32,10,'N'),(155,34,34,'W')]" + }, + { + "objectID": "basic_concepts/lambda_functions.html#syntax", + "href": "basic_concepts/lambda_functions.html#syntax", + "title": "20  Lambda functions", + "section": "Syntax", + "text": "Syntax\nlambda arguments: expression\nLet’s learn about Lambda functions by coding a few examples. We will start by importing the Numpy module, which we will need for some operations.\n\n# Import modules\nimport numpy as np" + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-1-convert-degrees-fahrenheit-to-celsius", + "href": "basic_concepts/lambda_functions.html#example-1-convert-degrees-fahrenheit-to-celsius", + "title": "20  Lambda functions", + "section": "Example 1: Convert degrees Fahrenheit to Celsius", + "text": "Example 1: Convert degrees Fahrenheit to Celsius\nConsider a scenario where you need to convert temperatures from degrees Fahrenheit to degrees Celsius frequently. Instead of repeatedly typing the conversion formula, you can encapsulate the expression in a lambda function for easy reuse. This approach not only avoids code repetition, but also reduces the risk of errors, as you write the formula just once and then call the lambda function by its name whenever needed.\nC = \\frac{5}{9}(F-32)\nwhere F is temperature in degrees Fahrenheit and C is the resulting temperature in degrees Celsius.\n\n# Define the lambda function (note the bold green reserved word)\nfahrenheit_to_Celsius = lambda F: 5/9 * (F-32)\n\n\n# Call the function\nF = 212 # temperature in degrees Fahrenheit\nC = fahrenheit_to_Celsius(212)\n\nprint(f\"A temperature of {F:.1f} °F is equivalent to {C:.1f} °C\")\n\nA temperature of 212.0 °F is equivalent to 100.0 °C" + }, + { + "objectID": "basic_concepts/lambda_functions.html#breakdown-of-lambda-function-components", + "href": "basic_concepts/lambda_functions.html#breakdown-of-lambda-function-components", + "title": "20  Lambda functions", + "section": "Breakdown of Lambda function components", + "text": "Breakdown of Lambda function components\nSimilar to what we did with regular functions, let’s breakdown the components of a lambda function.\n\nDefining the Lambda function:\nfahrenheit_to_Celsius = lambda F: 5/9 * (F-32)\nThis line defines a lambda function named fahrenheit_to_Celsius.\nLambda keyword:\n\nlambda: This is the keyword that signifies the start of a lambda function in Python. It’s followed by the parameters and the expression that makes up the function.\n\nParameter:\n\nF: This represents the parameter of the lambda function. In this case, F is the input temperature in degrees Fahrenheit that you want to convert to Celsius.\n\nFunction expression:\n\n5/9*(F-32): This is the expression that gets executed when the lambda function is called. It’s the formula for converting degrees Fahrenheit to Celsius.\n\n\n\n\n\n\n\n\nNote\n\n\n\nNote that, other than a simple line comment, Lambda functions offer very limited possibilities to provide associated documentation for the code, inputs, and outputs. If you need to a multi-line function or even a single-line function with detailed documentation, then a regular Python function is the way to go." + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-2-estimate-atmospheric-pressure-from-altitude", + "href": "basic_concepts/lambda_functions.html#example-2-estimate-atmospheric-pressure-from-altitude", + "title": "20  Lambda functions", + "section": "Example 2: Estimate atmospheric pressure from altitude", + "text": "Example 2: Estimate atmospheric pressure from altitude\nEstimating atmospheric pressure from altitude (learn more about the topic here) is a classic problem in environmental science and meteorology, and using Python functions is an excellent tool for solving it.\nP = 101.3 \\; exp\\Bigg(\\frac{-h}{8400} \\Bigg) \nwhere P is atmospheric pressure in kPa and h is altitude above sea level in meters. The coefficient 8400 is the result of aggregating the values for the gravitational acceleration, the molar mass of dry air, the universal gas constant, and sea level standard temperature; and converting from Pa to kPa.\n\n# Define lambda function\nestimate_atm_pressure = lambda A: 101.3 * np.exp(-A/8400) # atm pressure in kPa\n\n\n# Compute atmospheric pressure for Kansas City, KS\ncity = \"Kansas City, KS\"\nh = 280 # meters a.s.l.\nP = estimate_atm_pressure(h)\n\nprint(f\"Pressure in {city} at an elevation of {h} m is {P:.1f} kPa\")\n\nPressure in Kansas City, KS at an elevation of 280 m is 98.0 kPa\n\n\n\n# Compute atmospheric pressure for Denver, CO\ncity = \"Denver, CO\"\nh = 1600 # meters a.s.l. \nP = estimate_atm_pressure(h)\n\nprint(f\"Pressure in {city} at an elevation of {h} m is {P:.1f} kPa.\")\n\nPressure in Denver, CO at an elevation of 1600 m is 83.7 kPa." + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-3-volume-of-tree-trunk", + "href": "basic_concepts/lambda_functions.html#example-3-volume-of-tree-trunk", + "title": "20  Lambda functions", + "section": "Example 3: Volume of tree trunk", + "text": "Example 3: Volume of tree trunk\nIn the fields of forestry and ecology, trunk volume is a key metric for assessing and monitoring tree size. While tree trunks can have complex shapes, their general resemblance to cones allows for reasonably accurate volume estimations using basic measurements like trunk diameter and height. You can learn more about measuring tree volumes here. To calculate trunk volume, we can use a simplified formula:\n V = \\frac{\\pi \\; h \\; (D_1^2 + D_1^2 + D_1 D_2)}{12} \nwhere D_1, D_2 are the diameters of the top and bottom circular cross-sections of the tree.\nLet’s implement this formula using a Lambda function and then I propose computing the approximate volume of a Giant Sequoia. The General Sherman, located in the Sequoia National Park in California, is the largest single-stem living tree on Earth with an approximate hight of 84 m and a diameter at breast height of about 7.7 m. This example for computing tree volumes illustrates the practical application of programming in environmental science.\n\ncompute_trunk_volume = lambda d1, d2, h: (np.pi * h * (d1**2 + d2**2 + d1*d2))/12\n\n# Approximate tree dimensions (these values result\ndiameter_base = 7.7 # meters\ndiamter_top = 1 # meters (I assumed this value for the top of the stem)\nheight = 84 # meters\n\n# Compute volume. The results is remarkably similar tot he reported 1,487 m^3\ntrunk_volume = compute_trunk_volume(diameter_base, diamter_top, height) # m^3\n\nprint(f\"The trunk volume of a Giant Sequoia is {trunk_volume:.0f} cubic meters\")\n\n# Find relative volume compared to an Olympic-size (50m x 25m x 2m) swimming pool.\npool_volume = 50 * 25 * 2\nrel_volume = trunk_volume/pool_volume*100 # Relative volume trunk/pool\n\n# Ratio of volumes\nprint(f\"Trunk volume is {rel_volume:.1f}% the size of an Olympic-size pool\")\n\nThe trunk volume of a Giant Sequoia is 1495 cubic meters\nTrunk volume is 59.8% the size of an Olympic-size pool" + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-4-calculate-the-sodium-adsorption-ratio-sar.", + "href": "basic_concepts/lambda_functions.html#example-4-calculate-the-sodium-adsorption-ratio-sar.", + "title": "20  Lambda functions", + "section": "Example 4: Calculate the sodium adsorption ratio (SAR).", + "text": "Example 4: Calculate the sodium adsorption ratio (SAR).\nThe Sodium Adsorption Ratio (SAR) is a water quality indicator for determining the suitability for agricultural irrigation. In high concetrations, sodium has a negative impact on plant growth and disperses soil colloids, which has a detrimental impact on soil aggregation, soil structure and infiltration. It uses the concentrations of sodium, calcium, and magnesium ions measured in milliequivalents per liter (meq/L) to calculate the SAR value based on the formula:\n SAR = \\frac{\\text{Na}^+}{\\sqrt{\\frac{\\text{Ca}^{2+} + \\text{Mg}^{2+}}{2}}}\n\n# Define lambda function\ncalculate_sar = lambda na,ca,mg: na / ((ca+mg) / 2)**0.5\n\n\n# Determine SAR value for a water sample with the following ion concentrations\n\nna = 10 # Sodium ion concentration in meq/L\nca = 5 # Calcium ion concentration in meq/L\nmg = 2 # Magnesium ion concentration in meq/L\n\nsar_value = calculate_sar(na, ca, mg)\nprint(f\"Sodium Adsorption Ratio (SAR) is: {sar_value:.1f}\")\n\n# SAR values <5 are typically excellent for irrigation, \n# while SAR values >15 are typically unsuitable for irrigation of most crops.\n\nSodium Adsorption Ratio (SAR) is: 5.3" + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-5-compute-soil-porosity", + "href": "basic_concepts/lambda_functions.html#example-5-compute-soil-porosity", + "title": "20  Lambda functions", + "section": "Example 5: Compute soil porosity", + "text": "Example 5: Compute soil porosity\nSoil porosity refers to the percentage of the soil’s volume that is occupied by voids that can be occupied by air and water. Soil porosity is a soil physical property that conditions the soil’s ability to hold and transmit water, heat, and air, and is essential for root growth and microbial activity. For a given textural class, soils with lower porosity values tend to be compacted compared to soils with greater porosity values.\nPorosity (f, dimensionless) is defined as the volume of voids over the total volume of soil, and it can be approximated using bulk density (\\rho_b) and particle density (\\rho_s, often assumed to have a value of 2.65 g cm^{-3} for mineral soils):\n f = \\Bigg( 1 - \\frac{\\rho_b}{\\rho_s} \\Bigg)\n\n# Define lambda function\n# bd is the bulk density\ncalculate_porosity = lambda bd: 1 - (bd/2.65)\n\n# Compute soil porosity\nbd = 1.35 # Bulk density in g/cm^3\nf = calculate_porosity(bd)\n\n# Print result\nprint(f'The porosity of a soil with a bulk density of {bd:.2f} g/cm^3 is {f*100:.1f} %')\n\nThe porosity of a soil with a bulk density of 1.35 g/cm^3 is 49.1 %" + }, + { + "objectID": "basic_concepts/lambda_functions.html#example-6-estimate-osmotic-potential", + "href": "basic_concepts/lambda_functions.html#example-6-estimate-osmotic-potential", + "title": "20  Lambda functions", + "section": "Example 6: Estimate osmotic potential", + "text": "Example 6: Estimate osmotic potential\nIn soil science, osmotic potential (\\psi_o, kPa) represents the measure of the soil’s capacity to retain water due to the presence of dissolved solutes. Osmotic potential directly influences soil moisture dynamics and plant water uptake, impacting crop productivity and ecosystem health. For instance, soils with lower osmotic potential (i.e., more negative values), typically resulting from a higher concentration of solutes, create a greater challenge for plants to extract water, potentially impacting their growth and survival. The osmotic potential of the soil solution can be approximated based on measurements of the bulk electrical conductivity of a saturation paste extract and volumetric water content using the following formula:\n \\psi_{o} = 36 \\ EC_b \\ \\frac{\\theta_s}{\\theta}\nwhere EC_b is the bulk electrical conductivity of the saturation paste extract in dS m^{-1}, \\theta_s is the voluemtric water content at saturation in cm^3 cm^{-3}, and \\theta is the volumetric water content cm^3 cm^{-3}. The term 36 \\ EC_b represents the osmotic potential at saturation \\psi_{os}\n\n# Define lambda function\ncalculate_osmotic = lambda theta,theta_s,EC: 36 * EC * theta_s/theta\n\n# Compute osmotic potential\ntheta_s = 0.45 # cm^3/cm^3\ntheta = 0.15 # cm^3/cm^3\nEC = 1 # dS/m\n\npsi_o = calculate_osmotic(theta,theta_s,EC)\nprint(f\"Osmotic potential is {psi_o:.1f} kPa\")\n\nOsmotic potential is 108.0 kPa" + }, + { + "objectID": "basic_concepts/lambda_functions.html#practice", + "href": "basic_concepts/lambda_functions.html#practice", + "title": "20  Lambda functions", + "section": "Practice", + "text": "Practice\n\nImplement Stoke’s Law as a lambda function. Then, call the function to estimate the terminal velocity of a 1 mm diamter sand particle in water and a 1 mm raindrop in air.\nImplement a quadratic polynomial describing a yield-nitrogen response curve. Here is one model that you can try, but there are many empirical yield-N relationships in the literature: y = -0.0034 x^2 + 0.9613 x + 115.6" + }, + { + "objectID": "basic_concepts/inputs.html#practice", + "href": "basic_concepts/inputs.html#practice", + "title": "21  Inputs", + "section": "Practice", + "text": "Practice\n\nCreate a script that computes the soil bulk density, porosity, gravimetric water content, and volumetric water content given the mass of wet soil, mass of oven-dry soil, and the volume of the soil. Your code should contain a function that includes an optional input parameter for particle density with a default value of 2.65 g/cm^3." + }, + { + "objectID": "basic_concepts/for_loop.html#syntax", + "href": "basic_concepts/for_loop.html#syntax", + "title": "22  For loop", + "section": "Syntax", + "text": "Syntax\nfor item in iterable:\n # Code block to execute for each item" + }, + { + "objectID": "basic_concepts/for_loop.html#example-1-basic-for-loop", + "href": "basic_concepts/for_loop.html#example-1-basic-for-loop", + "title": "22  For loop", + "section": "Example 1: Basic For loop", + "text": "Example 1: Basic For loop\nSuppose we have a list of soil nitrogen levels from different test sites and we want to print each value. Here’s how you can do it:\n\n# Example of a for loop\n\n# List of soil nitrogen levels in mg/kg\nnitrogen_levels = [15, 20, 10, 25, 18]\n\n# Iterating through the list\nfor level in nitrogen_levels:\n print(f\"Soil Nitrogen Level: {level} mg/kg\")\n\nSoil Nitrogen Level: 15 mg/kg\nSoil Nitrogen Level: 20 mg/kg\nSoil Nitrogen Level: 10 mg/kg\nSoil Nitrogen Level: 25 mg/kg\nSoil Nitrogen Level: 18 mg/kg" + }, + { + "objectID": "basic_concepts/for_loop.html#example-2-for-loop-using-the-enumerate-function", + "href": "basic_concepts/for_loop.html#example-2-for-loop-using-the-enumerate-function", + "title": "22  For loop", + "section": "Example 2: For loop using the enumerate function", + "text": "Example 2: For loop using the enumerate function\nThe enumerate function adds a counter to the loop, providing the index position along with the value. This is helpful when you need to access the position of the elements as you iterate.\nLet’s modify the previous example to include the sample number using enumerate:\n\n# Iterating through the list with enumerate\nfor index, level in enumerate(nitrogen_levels):\n print(f\"Sample {index + 1}: Soil Nitrogen Level = {level} mg/kg\")\n\nSample 1: Soil Nitrogen Level = 15 mg/kg\nSample 2: Soil Nitrogen Level = 20 mg/kg\nSample 3: Soil Nitrogen Level = 10 mg/kg\nSample 4: Soil Nitrogen Level = 25 mg/kg\nSample 5: Soil Nitrogen Level = 18 mg/kg\n\n\nIn this example, index represents the position of each element in the list (starting from 0), and level is the nitrogen level. We use index + 1 in the print statement to start the sample numbering from 1 instead of 0." + }, + { + "objectID": "basic_concepts/for_loop.html#example-3-combine-for-loop-with-if-statement", + "href": "basic_concepts/for_loop.html#example-3-combine-for-loop-with-if-statement", + "title": "22  For loop", + "section": "Example 3: Combine for loop with if statement", + "text": "Example 3: Combine for loop with if statement\nCombining a for loop with if statements unleashes a powerful and precise control over data processing and decision-making within iterative sequences. The for loop provides a structured way to iterate over a range of elements in a collection, such as lists, tuples, or strings. When an if statement is nested within this loop, it introduces conditional logic, allowing the program to execute specific blocks of code only when certain criteria are met. This combination is incredibly versatile: it can be used for filtering data, conditional aggregation of data, and applying different operations to elements based on specific conditions.\n\nExample 3a\nIn this short example we will combine a for loop with if statements to generate the complementary DNA strand by iterating over each nucleotide. The code will also filter if there is an incorrect base and in which position that incorrect base is located.\n\n# Example of DNA strand\nstrand = 'ACCTTATCGGC'\n\n# Create an empty complementary strand\nstrand_c = ''\n\n# Iterate over each base in the DNA strand (a string)\nfor k,base in enumerate(strand):\n \n if base == 'A':\n strand_c += 'T'\n \n elif base == 'T':\n strand_c += 'A'\n \n elif base == 'C':\n strand_c += 'G'\n \n elif base == 'G':\n strand_c += 'C'\n \n else:\n print('Incorrect base', base, 'in position', k+1)\n \nprint(strand_c)\n\nTGGAATAGCCG\n\n\nTry inserting or changing one of the bases in the sequence for another character not representing a DNA nucleotide.\n\n\nExample 3b\nIn this example we will compute the total number of growing degree days for corn over the period of one week based on daily average air temperatures.\n\n# Define daily temperatures for a week \nT_daily = [6, 12, 18, 8, 22, 19, 16] # degrees Celsius\n\n# Define base temperature for corn\nT_base = 8 # degrees Celsius\n\n# Initialize growing degree days accumulator\ngdd = 0\n\n# Loop through each day of the week\nfor T in T_daily:\n\n if T > T_base:\n gdd_daily = T - T_base\n else:\n gdd_daily = 0\n\n # Accumulate daily growing degree days\n gdd += gdd_daily\n\n# Output total growing degree days for the week\nprint(f\"Total Growing Degree Days for the Week: {gdd} Celsius-Days\")\n\nTotal Growing Degree Days for the Week: 47 Celsius-Days" + }, + { + "objectID": "basic_concepts/for_loop.html#example-4-for-loop-using-a-dictionary", + "href": "basic_concepts/for_loop.html#example-4-for-loop-using-a-dictionary", + "title": "22  For loop", + "section": "Example 4: For loop using a dictionary", + "text": "Example 4: For loop using a dictionary\n\n# Record air temperatures for a few cities in Kansas\nkansas_weather = {\n \"Topeka\": {\"Record High Temperature\": 40, \"Date\": \"July 20, 2003\"},\n \"Wichita\": {\"Record High Temperature\": 42, \"Date\": \"August 8, 2010\"},\n \"Lawrence\": {\"Record High Temperature\": 39, \"Date\": \"June 15, 2006\"},\n \"Manhattan\": {\"Record High Temperature\": 41, \"Date\": \"July 18, 2003\"}\n}\n\n# Iterating through the dictionary\nfor city, weather_details in kansas_weather.items():\n print(f\"Record Weather in {city}:\")\n print(f\" High Temperature: {weather_details['Record High Temperature']}°C\")\n print(f\" Date of Occurrence: {weather_details['Date']}\")\n \n\nRecord Weather in Topeka:\n High Temperature: 40°C\n Date of Occurrence: July 20, 2003\nRecord Weather in Wichita:\n High Temperature: 42°C\n Date of Occurrence: August 8, 2010\nRecord Weather in Lawrence:\n High Temperature: 39°C\n Date of Occurrence: June 15, 2006\nRecord Weather in Manhattan:\n High Temperature: 41°C\n Date of Occurrence: July 18, 2003\n\n\nThe .items() method of a dictionary returns a view object as a list of tuples representing the key-value pairs of the dictionary. So, we can assign the key to one variable and the value to another variable when defining the for loop.\nThink of a view object as a window into the original data structure. It doesn’t create a new copy of the data. View objects are useful because they allow you to work with the data in a flexible and memory-efficient way, and they are especially handy for working with large datasets.\nView objects do not support indexing directly like lists or tuples. If you need to access specific elements by index frequently, you should consider converting the view object to a list or tuple first.\n\n# Show the content returned by .items()\nprint(kansas_weather.items())\n\ndict_items([('Topeka', {'Record High Temperature': 40, 'Date': 'July 20, 2003'}), ('Wichita', {'Record High Temperature': 42, 'Date': 'August 8, 2010'}), ('Lawrence', {'Record High Temperature': 39, 'Date': 'June 15, 2006'}), ('Manhattan', {'Record High Temperature': 41, 'Date': 'July 18, 2003'})])\n\n\nFirst item: ('Topeka', {'Record High Temperature': 40, 'Date': 'July 20, 2003'})\nkey: 'Topeka'\nvalue: {'Record High Temperature': 40, 'Date': 'July 20, 2003'}" + }, + { + "objectID": "basic_concepts/for_loop.html#example-5-nested-for-loops", + "href": "basic_concepts/for_loop.html#example-5-nested-for-loops", + "title": "22  For loop", + "section": "Example 5: Nested for loops", + "text": "Example 5: Nested for loops\nImagine we are analyzing soil samples from different fields. Each field has multiple samples, and each sample has various measurements. We’ll use nested for loops to iterate through the fields and then through each measurement in the samples.\n\n# Soil data from multiple fields\nsoil_data = {\n \"Field 1\": [\n {\"pH\": 6.5, \"Moisture\": 20, \"Nitrogen\": 3},\n {\"pH\": 6.8, \"Moisture\": 22, \"Nitrogen\": 3.2}\n ],\n \"Field 2\": [\n {\"pH\": 7.0, \"Moisture\": 18, \"Nitrogen\": 2.8},\n {\"pH\": 7.1, \"Moisture\": 19, \"Nitrogen\": 2.9}\n ]\n}\n\n# Iterating through each field\nfor field, samples in soil_data.items():\n print(f\"Data for {field}:\")\n \n # Nested loop to iterate through each sample in the field\n for sample in samples:\n print(f\" Sample - pH: {sample['pH']}, Moisture: {sample['Moisture']}%, Nitrogen: {sample['Nitrogen']}%\")\n\nData for Field 1:\n Sample - pH: 6.5, Moisture: 20%, Nitrogen: 3%\n Sample - pH: 6.8, Moisture: 22%, Nitrogen: 3.2%\nData for Field 2:\n Sample - pH: 7.0, Moisture: 18%, Nitrogen: 2.8%\n Sample - pH: 7.1, Moisture: 19%, Nitrogen: 2.9%\n\n\nIn this example, soil_data is a dictionary where each key is a field, and the value is a list of soil samples (each sample is a dictionary of measurements). The first for loop iterates over the fields, and the nested loop iterates over the samples within each field, printing out the pH, Moisture, and Nitrogen content for each sample." + }, + { + "objectID": "basic_concepts/for_loop.html#example-6-for-loop-using-break-and-continue", + "href": "basic_concepts/for_loop.html#example-6-for-loop-using-break-and-continue", + "title": "22  For loop", + "section": "Example 6: For loop using break and continue", + "text": "Example 6: For loop using break and continue\nImagine we are evaluating crop yields from different fields. We want to stop processing if we encounter a field with exceptionally low yield (signifying a possible data error or a major issue with the field) and skip over fields with average yields to focus on fields with exceptionally high or low yields.\n\n# Crop yield data (in tons per hectare) for different fields\ncrop_yields = {\"Field 1\": 2.5, \"Field 2\": 3.2, \"Field 3\": 1.0, \"Field 4\": 3.8, \"Field 5\": 0.8}\n\n# Thresholds for yield consideration\nlow_yield_threshold = 1.5\nhigh_yield_threshold = 3.0\n\nfor field, yield_data in crop_yields.items():\n if (yield_data < low_yield_threshold) or (yield_data > high_yield_threshold):\n print(f\"{field} is a potential outlier: {yield_data} tons/ha\")\n break # Stop processing further as this could indicate a major issue\n else:\n continue\n\n\nField 2 is a potential outlier: 3.2 tons/ha\n\n\nWe use break to stop the iteration when we encounter a yield below the low_yield_threshold or above high_yield_threshold, which could indicate an outlier that requires immediate attention.\nWe use continue to skip to the next iteration without executing any additional code in hte loop." + }, + { + "objectID": "basic_concepts/for_loop.html#compative-anatomy-of-for-loops", + "href": "basic_concepts/for_loop.html#compative-anatomy-of-for-loops", + "title": "22  For loop", + "section": "Compative anatomy of for loops", + "text": "Compative anatomy of for loops\n\nPython\nnitrogen_levels = [15, 20, 10, 25, 18]\nfor level in nitrogen_levels:\n print(f\"Soil Nitrogen Level: {level} mg/kg\")\n\n\nMatlab\nnitrogen_levels = [15, 20, 10, 25, 18];\nfor level = nitrogen_levels\n fprintf('Soil Nitrogen Level: %d mg/kg\\n', level);\nend\n\n\nJulia\nnitrogen_levels = [15, 20, 10, 25, 18]\nfor level in nitrogen_levels\n println(\"Soil Nitrogen Level: $level mg/kg\")\nend\n\n\nR\nnitrogen_levels <- c(15, 20, 10, 25, 18)\nfor (level in nitrogen_levels) {\n cat(\"Soil Nitrogen Level:\", level, \"mg/kg\\n\")\n}\n\n\nJavaScript\nconst nitrogen_levels = [15, 20, 10, 25, 18];\nfor (let level of nitrogen_levels) {\n console.log(`Soil Nitrogen Level: ${level} mg/kg`);\n}\n\n\nCommonalities among programming languages:\n\nAll languages use a for keyword to start the loop.\nThey iterate over a collection of items, like an array or list.\nEach language uses a variable to represent the current item in each iteration.\nThe body of the loop (code to be executed) is enclosed within a block defined by indentation or brackets." + }, + { + "objectID": "basic_concepts/list_comprehensions.html#example-1-convert-temperature-units", + "href": "basic_concepts/list_comprehensions.html#example-1-convert-temperature-units", + "title": "23  List comprehensions", + "section": "Example 1: Convert temperature units", + "text": "Example 1: Convert temperature units\nIf you have a list of daily average temperatures in Celsius from various farm locations, you can convert them to Fahrenheit using a list comprehension\n\n# Convert temperatures\ncelsius = [22, 18, 25] # Example temperatures in Celsius\nfahrenheit = [(c * 9/5) + 32 for c in celsius]\nprint(fahrenheit)\n\n[71.6, 64.4, 77.0]" + }, + { + "objectID": "basic_concepts/list_comprehensions.html#example-2-conver-from-lbsacre-to-kgha", + "href": "basic_concepts/list_comprehensions.html#example-2-conver-from-lbsacre-to-kgha", + "title": "23  List comprehensions", + "section": "Example 2: Conver from lbs/acre to kg/ha", + "text": "Example 2: Conver from lbs/acre to kg/ha\nSuppose you have a list of crop yields in bushels per acre for a farm. You can easily calculate the yields in kilograms per hectare if you know the conversion factors between units.\n\nbushels_per_acre = [120, 150, 180] # Example yields in bushels\nkg_per_bushel = 25.4 # Conversion factor for corn\nacres_per_hectare = 0.405\n\nkg_per_hectare = [round(yld * kg_per_bushel / acres_per_hectare) for yld in bushels_per_acre]\nprint(kg_per_hectare)\n\n[7526, 9407, 11289]" + }, + { + "objectID": "basic_concepts/list_comprehensions.html#example-3-classify-soil-ph", + "href": "basic_concepts/list_comprehensions.html#example-3-classify-soil-ph", + "title": "23  List comprehensions", + "section": "Example 3: Classify soil pH", + "text": "Example 3: Classify soil pH\nThis example is a bit more advanced and also includes if statements.\n\n# Classify soil pH into acidic, neutral, or alkaline.\nsoil_ph = [6.5, 7.0, 8.2] \nph_class = ['acidic' if ph < 7 else 'alkaline' if ph > 7 else 'neutral' for ph in soil_ph]\nprint(ph_class)\n\n['acidic', 'neutral', 'alkaline']" + }, + { + "objectID": "basic_concepts/while_loop.html#syntax", + "href": "basic_concepts/while_loop.html#syntax", + "title": "24  While loop", + "section": "Syntax", + "text": "Syntax\nwhile condition:\n # Code to execute repeatedly\ncondition: A Boolean expression that determines whether the loop continues.\nLet’s look at a trivial example:\n\nA = 0\nwhile A < 3:\n print(A)\n A += 1\n \n\n0\n1\n2\n\n\nThis example will only print values 0, 1, and 2. As soon as the third iteration ends, the value of A becomes 3, the condition that A<3 is no longer true, and the while loop breaks before starting the fourth iteration." + }, + { + "objectID": "basic_concepts/while_loop.html#example-1-irrigation-decision", + "href": "basic_concepts/while_loop.html#example-1-irrigation-decision", + "title": "24  While loop", + "section": "Example 1: Irrigation decision", + "text": "Example 1: Irrigation decision\nIn this example we simulate a simple irrigation system that adjusts water application based on the soil moisture level, ensuring that the crop receives adequate water. The system checks the soil moisture level every day and decides whether to irrigate based on the soil moisture level. We assume that evapotranspiration reduces the moisture level daily, and that irrigation increases the soil moisture.\n\nsoil_moisture = 90 # initial soil moisture (mm)\nmoisture_threshold = 70 # moisture below which irrigation is needed (mm)\nirrigation_amount = 20 # irrigation amount (mm)\ndaily_et = 5 # daily evapotranspiration (mm)\n\n# Simulate daily check over a 10-day period\nday = 0\nwhile day < 10:\n print(f\"Day {day + 1}: Soil moisture is {soil_moisture}%.\")\n \n # Check if irrigation is needed\n if soil_moisture < moisture_threshold:\n print(\"Irrigating...\")\n soil_moisture += irrigation_amount\n print(f\"Soil moisture after irrigation is {soil_moisture}%.\")\n else:\n print(\"No irrigation needed today.\")\n \n # Update soil moisture for next day\n soil_moisture -= daily_et\n day += 1\n\nDay 1: Soil moisture is 90%.\nNo irrigation needed today.\nDay 2: Soil moisture is 85%.\nNo irrigation needed today.\nDay 3: Soil moisture is 80%.\nNo irrigation needed today.\nDay 4: Soil moisture is 75%.\nNo irrigation needed today.\nDay 5: Soil moisture is 70%.\nNo irrigation needed today.\nDay 6: Soil moisture is 65%.\nIrrigating...\nSoil moisture after irrigation is 85%.\nDay 7: Soil moisture is 80%.\nNo irrigation needed today.\nDay 8: Soil moisture is 75%.\nNo irrigation needed today.\nDay 9: Soil moisture is 70%.\nNo irrigation needed today.\nDay 10: Soil moisture is 65%.\nIrrigating...\nSoil moisture after irrigation is 85%." + }, + { + "objectID": "basic_concepts/while_loop.html#example-2-guess-the-soil-taxonomic-order", + "href": "basic_concepts/while_loop.html#example-2-guess-the-soil-taxonomic-order", + "title": "24  While loop", + "section": "Example 2: Guess the soil taxonomic order", + "text": "Example 2: Guess the soil taxonomic order\nThe soil taxonomic orders form the highest category in the soil classification system, each order representing distinct characteristics and soil formation processes. The twelve orders provide a framework for understanding soil properties and their implications for agriculture, environmental management, and land use. This website has great pictures of real soil profiles and additional useful information.\nIt’s always fun to play games, but crafting your own is even better. This exercise was written so that you can change the information contained in the database dictionary with some other data that sparks your interest. For soil scientists, this is an excellent opportunity to review your knowledge about the soil taxonomy.\n\nimport random\n\n# Dictionary of soil orders with a hint\ndatabase = {\n \"Alfisols\": [\"Fertile, with subsurface clay accumulation.\", \"Starts with 'A'\"],\n \"Andisols\": [\"Formed from volcanic materials, high in organic matter.\", \"Starts with 'A'\"],\n \"Aridisols\": [\"Dry soils, often found in deserts.\", \"Starts with 'A'\"],\n \"Entisols\": [\"Young soils with minimal horizon development.\", \"Starts with 'E'\"],\n \"Gelisols\": [\"Contain permafrost, found in cold regions.\", \"Starts with 'G'\"],\n \"Histosols\": [\"Organic, often water-saturated soils like peat.\", \"Starts with 'H'\"],\n \"Inceptisols\": [\"Young, with weak horizon development.\", \"Starts with 'I'\"],\n \"Mollisols\": [\"Dark, rich in organic matter, found under grasslands.\", \"Starts with 'M'\"],\n \"Oxisols\": [\"Highly weathered, tropical soils.\", \"Starts with 'O'\"],\n \"Spodosols\": [\"Acidic, with organic and mineral layers.\", \"Starts with 'S'\"],\n \"Ultisols\": [\"Weathered, acidic soils with subsurface clay.\", \"Starts with 'U'\"],\n \"Vertisols\": [\"Rich in clay, expand and contract with moisture.\", \"Starts with 'V'\"]\n}\n\n\n# Select a random soil taxonomic order and its hints\n# Each item is a list of tuples\nselection, hints = random.choice(list(database.items()))\nhints_iter = iter(hints)\n\nprint(\"Guess the soil taxonomic order! Type 'hint' for a hint.\")\n\n# Initial hint\nprint(\"First Hint:\", next(hints_iter))\n\n# While loop for guessing game\nwhile True:\n guess = input(\"Your guess: \").strip().lower()\n\n if guess == selection.lower():\n print(f\"Correct! It was {selection}.\")\n break\n \n elif guess == \"hint\":\n try:\n print(\"Hint:\", next(hints_iter))\n except StopIteration:\n print(\"No more hints. Please make a guess.\")\n else:\n print(\"Incorrect, try again or type 'hint' for another hint.\")\n\nGuess the soil taxonomic order! Type 'hint' for a hint.\nFirst Hint: Highly weathered, tropical soils.\n\n\nYour guess: Oxisols\n\n\nCorrect! It was Oxisols.\n\n\n\nExplanation\nThe iter() function is used to create an iterator from an iterable object, like a list or a dictionary. Once you have an iterator, you can use the next() function to sequentially access elements from the iterator. Each call to next() retrieves the next element in the sequence. When next() reaches the end of the sequence and there are no more elements to return, it raises a StopIteration exception. This combination of a loop and iter() allows for a controlled iteration process, especially useful in situations where you need to process elements one at a time.\nThe strip() method removes any leading and trailing whitespace (like spaces, tabs, or new lines) from the input string. This is helpful to ensure that extra spaces do not affect the comparison of the user’s guess to the correct answer.\nThe lower() method then converts the string to lowercase. This ensures that the comparison is case-insensitive, meaning that “Oxisols”, “oxisols”, and “OXISOLS” are all treated as the same guess." + }, + { + "objectID": "basic_concepts/while_loop.html#references", + "href": "basic_concepts/while_loop.html#references", + "title": "24  While loop", + "section": "References", + "text": "References\n\nThe twelve orders of soil taxonomy. United States Department of Agriculture website: https://www.nrcs.usda.gov/resources/education-and-teaching-materials/the-twelve-orders-of-soil-taxonomy. Accessed on 8 January 2024" + }, + { + "objectID": "basic_concepts/directory_navigation.html#os-module", + "href": "basic_concepts/directory_navigation.html#os-module", + "title": "25  Directory navigation", + "section": "os module", + "text": "os module\nThe operating, os, module provides a way of using operating system-dependent functionality like reading or writing to the file system, managing file and directory paths, and working with file permissions.\n\n# Import module\nimport os\n\n\n# Finding current working directory\ncurrent_dir = os.getcwd()\nprint(\"Current Working Directory:\", current_dir)\n\nCurrent Working Directory: /Users/andrespatrignani/Soil Water Lab Dropbox/Andres Patrignani/Teaching/Scientific programming/pynotes-agriscience/basic_concepts\n\n\n\n# Move up one directory (exit current folder)\nos.chdir('..')\n\n# Changing Directory\nos.chdir('/path/to/new/directory') # Navigate to a new directory\n\n# Create new directory\nnew_dir = os.path.join(current_dir, 'soil_data')\nos.makedirs(new_dir, exist_ok=True) # it will override if directory already exists\n\n# Remove empty directory\ndir_to_remove = os.path.join(os.getcwd(), 'soil_data')\nos.rmdir(dir_to_remove)\n\n# Remove directory containing files (need to import shutil module)\n# shutil.rmtree(dir_to_remove)\n\n\n# Splitting file names into parts\nfile_name = \"soil_samples.csv\"\nfile_path = os.path.join(current_dir, file_name)\ndir_name, file_name = os.path.split(file_path)\nfile_name_only, file_extension = os.path.splitext(file_name)\nprint(f\"Directory:{dir_name} \\nFile Name:{file_name_only} \\nExtension:{file_extension}\")\n\n\n# List specific files\nfiles_list = os.listdir(current_dir)\nfor file in files_list:\n if file.endswith(\".csv\"):\n print(file)" + }, + { + "objectID": "basic_concepts/directory_navigation.html#using-the-glob-module", + "href": "basic_concepts/directory_navigation.html#using-the-glob-module", + "title": "25  Directory navigation", + "section": "Using the glob module", + "text": "Using the glob module\nThe glob module is similar to the os module, but is widely used for file name pattern matching and allows you to search for files and directories using a convenient wildcard matching that is particularly handy for listing files in a directory, especially when you’re interested in files of a specific type (such as csv, txt, jpg). To target files with a specific extension, we use a wildcard symbol (*), which acts as a placeholder for 'all files of a particular type'. For example, to list all the text files in a given directory, we use the pattern *.txt.\n\n# Import module\nimport glob\n\n\n# Get current working directory\nprint(glob.os.getcwd())\n\n# Move up one directory (exit current folder)\nglob.os.chdir('..')\n\n# Get all files in directory\nprint(glob.os.listdir())\n\n# List and store all files with specific file extension\ntxt_files = [file in glob.glob(\"*.csv\")] # This is a list comprehension" + }, + { + "objectID": "basic_concepts/objects_and_classes.html#syntax", + "href": "basic_concepts/objects_and_classes.html#syntax", + "title": "26  Objects and classes", + "section": "Syntax", + "text": "Syntax\nSyntax for accessing object properties and methods:\n object.property\n object.method()\nSyntax for defining our own objects using classes:\nclass ClassName:\n def __init__(self, attributes):\n # Constructor method for initializing instance attributes\n\n def method_name(self, parameters):\n # Method of the class\n \nClassName is the name of the class. __init__ is the constructor method. method_name is a method of the class.\nLet’s look at these concepts using a simple example and then we will create our own object." + }, + { + "objectID": "basic_concepts/objects_and_classes.html#properties-and-methods-example", + "href": "basic_concepts/objects_and_classes.html#properties-and-methods-example", + "title": "26  Objects and classes", + "section": "Properties and methods example", + "text": "Properties and methods example\nI often find that using the NumPy module is a simple and clear way to illustrate the difference between properties/attributes and methods/functions.\n\n\n\n\n\n\nJargon note\n\n\n\nThe terms property and attribute are used interchangeably to represent characteristics of the object.\nSimilarly, the term method is an another word to denote a function inside of an object. Methods (or functions) represent actions that can be performed on the object and are only available within a specific object.\n\n\n\nimport numpy as np\n\n# Define an array (this is an object)\nA = np.array([1, 2, 3, 4, 5, 6])\n\n# Properties of the Numpy array object\nprint('Properties')\nprint(A.shape) # Dimensions of the array\nprint(A.size) # Total number of elements\nprint(A.dtype) # Data type\nprint(A.ndim) # NUmber of array dimensions\n\n# Methods/Functions of the Numpy array object\nprint('') # Add a blank line\nprint('Methods')\nprint(A.mean()) # Method to compute average of all values\nprint(A.sum()) # Method to compute sum of all values\nprint(A.cumsum()) # Method to compute running sum of all values\nprint(A.reshape(3,2)) # Reshape to a 3 by 2 matrix\n\nProperties\n(6,)\n6\nint64\n1\n\nMethods\n3.5\n21\n[ 1 3 6 10 15 21]\n[[1 2]\n [3 4]\n [5 6]]" + }, + { + "objectID": "basic_concepts/objects_and_classes.html#class-example-laboratory-sample", + "href": "basic_concepts/objects_and_classes.html#class-example-laboratory-sample", + "title": "26  Objects and classes", + "section": "Class example: Laboratory sample", + "text": "Class example: Laboratory sample\nConsider a scenario where we operate a soil analysis laboratory receiving samples from various clients, such as farmers, gardeners, and golf course superintendents. Each soil sample possesses unique attributes that we need to record and analyze. These attributes might include the client’s full name, the date the sample was received, a unique identifier, the location of sample collection, the analyses requested, and the results of these analyses. In this context, the primary unit of interest is the individual sample, and all the additional information represents its metadata.\nTo efficiently manage this data, we can create a Python class specifically for our soil samples. This class will allow us to create a structured record for each new sample we receive, demonstrating Python’s flexibility in creating custom objects tailored to our specific needs. We will use Python’s uuid module to generate a unique identifier for each sample and the datetime module to timestamp when each sample object is created.\n\n\n\n\n\n\nNote\n\n\n\nClasses are like blueprints for objects since they define what properties and methods an object will have. For more information check Python’s official documentation about objects and classes\n\n\n\nimport uuid\nimport datetime\nimport pprint\n\nclass SoilSample:\n \"\"\" Class that defines attributes and methods for new soil samples\"\"\"\n \n def __init__(self, customer_name, location, analyses_requested):\n \"\"\"Attributes of the soil sample generated upon sample entry.\n \n Inputs\n ----------\n customer_name : string\n Customer's full name\n location : tuple\n Geographic location (lat,lon) of sample collection\n analyses_requested : list\n Requested analyses\n \"\"\"\n self.sample_id = str(uuid.uuid4()) # Unique identifier for each sample\n self.timestamp = datetime.datetime.now().strftime(\"%d-%b-%Y %H:%M:%S\") # Timestamp of sample entry\n self.customer_name = customer_name # Customer's full name\n self.location = location # Geographic location of sample collection\n self.analyses_requested = analyses_requested # List of requested analyses\n self.results = {} # Dictionary to store results of analyses\n\n def add_results(self, analysis_type, result):\n \"\"\"Function that adds the name and results of a specific soil analysis.\"\"\"\n self.results[analysis_type] = result # Add analysis results\n\n def summary(self):\n \"\"\"Function that prints summary information for the sample.\"\"\"\n info = (f\"Sample ID: {self.sample_id}\",\n f\"Timestamp: {self.timestamp}\",\n f\"Customer: {self.customer_name}\",\n f\"Location: {self.location}\",\n f\"Requested Analyses: {self.analyses_requested}\",\n f\"Results: {self.results}\")\n return pprint.pprint(info)\n\n\n\n\n\n\n\n\nWhat is __init__()?\n\n\n\nThe __init__() function at the beginning of the class is a special method that gets called (another term for this action is invoked) automatically when we create a new instance of the class. Think of __init__ as the setup of the object, where the initial state of a new object is defined by assigning values to its properties.\n\n\n\n\n\n\n\n\nWhat is self?\n\n\n\nWhen defining a class, the word self is used to refer to the instance of the class itself. It’s a way for the class to reference its own attributes and methods and is usually defined with the words self or this (but it can be anything else you want). Typically it is a short word that is meaningful and easy to type. We will use self to match the official Python documentation.\nImagine each class as a blueprint for building a house. Each house built from the blueprint is an instance (an occurrence) of the class. In this context, self is like saying this particular house, rather than the general blueprint.\n\n\n\n# Access our own documentation\nSoilSample?\n\n\nInit signature: SoilSample(customer_name, location, analyses_requested)\nDocstring: <no docstring>\nInit docstring:\nAttributes of the soil sample generated upon sample entry.\nInputs\n----------\ncustomer_name : string\n Customer's full name\nlocation : tuple\n Geographic location (lat,lon) of sample collection\nanalyses_requested : list\n Requested analyses\nType: type\nSubclasses: \n\n\n\n\n# Example usage. Create or instantiate a new object, in this case a new sample.\nnew_sample = SoilSample(\"Andres Patrignani\", (39.210089, -96.590213), [\"pH\", \"Nitrogen\"])\n\n\n\n\n\n\n\nWhat does instantiation mean?\n\n\n\nInstantiation is the term used in Python for the process of creating a new object from a blueprint, the class we just defined.\n\n\n\n# Access properties generated when we created the new sample\nprint(new_sample.customer_name)\nprint(new_sample.timestamp)\n\nAndres Patrignani\n04-Jan-2024 16:52:03\n\n\n\n# Use the add_results() method to add some information to our sample object\nnew_sample.add_results(\"pH\", 6.5)\nnew_sample.add_results(\"Nitrogen\", 20)\n\n\n# Use the summary() method to print the information available for our sample\nnew_sample.summary()\n\n('Sample ID: 322a5b20-ed71-4c4e-b181-789fb6574d8d',\n 'Timestamp: 04-Jan-2024 16:52:03',\n 'Customer: Andres Patrignani',\n 'Location: (39.210089, -96.590213)',\n \"Requested Analyses: ['pH', 'Nitrogen']\",\n \"Results: {'pH': 6.5, 'Nitrogen': 20}\")\n\n\n\n\n\n\n\n\nInheritance\n\n\n\nA cool feature of classes in Python is their ability to inherit properties and methods from an already pre-defined class. This is called inheritance, and it allows programmers to build upon and extend the functionality of existing classes, creating new, more specialized versions without reinventing the wheel. After mastering the basics of classes and objects, exploring class inheritance is a must to take your coding skills to the next level." + }, + { + "objectID": "basic_concepts/objects_and_classes.html#practice", + "href": "basic_concepts/objects_and_classes.html#practice", + "title": "26  Objects and classes", + "section": "Practice", + "text": "Practice\nTo take this exercise to the next level try the following improvements:\n\nAdd one new attribute and one new methods to the class\nUse the input() function to request the required data for each sample from users\nUse a for loop to pre-populate the results with None for each of the requested soil analyses. This will ensure that only those analyses are entered into the sample.\nCreate a way to store multiple sample entries. You can simply append each new sample to a variable defined at the beginning of your script, append the new entries to a text or .json file, use the pickle module, or use a databases like sqlite3, MySQL, or TinyDB module" + }, + { + "objectID": "basic_concepts/error_handling.html#syntax", + "href": "basic_concepts/error_handling.html#syntax", + "title": "27  Error handling", + "section": "Syntax", + "text": "Syntax\nThe primary constructs for error handling are try, except, else, and finally:\ntry: This block lets you test a block of code for errors.\nexcept: This block handles the error or specifies a response to specific error types.\nelse: (Optional) This block runs if no errors are raised in the try block.\nfinally: (Optional) This block executes regardless of the result of the try-except blocks.\ntry:\n # Code block where exceptions might occur\nexcept SomeException:\n # Code to handle the exception\nelse:\n # Code to execute if no exceptions occur (optional)\nfinally:\n # Code to execute regardless of exceptions (optional)\nIn addition to catching exceptions, Python allows programmers to raise their own exceptions using the raise statement. This can be useful for signaling specific error types in a way that is clear and tailored to your program’s needs.\nPython also has the isinstance() function that enables easy checking of strict data types. This is a Pythonic and handy method to validate input data types in functions." + }, + { + "objectID": "basic_concepts/error_handling.html#example-classification-of-soil-acidity-alkalinity", + "href": "basic_concepts/error_handling.html#example-classification-of-soil-acidity-alkalinity", + "title": "27  Error handling", + "section": "Example: Classification of soil acidity-alkalinity", + "text": "Example: Classification of soil acidity-alkalinity\nTo illustrate these error handling concepts, let’s write a simple function to determine whether a soil is acidic or alkaline based on its soil pH. This fucntion will not only require that we pass a numeric input to the function, but also that the range of soil pH is within 0 and 14.\n\ndef test_soil_pH(pH_value):\n \"\"\"\n Determines if soil is acidic, alkaline, or neutral based on its pH value.\n\n Parameters:\n pH_value (int, float, or str): The pH value of the soil. It should be a number \n or a string that can be converted to a float. \n Valid pH values range from 0 to 14.\n\n Returns:\n str: A description of the soil's acidity or alkalinity.\n \n Raises:\n TypeError: If the input is not an int, float, or string.\n ValueError: If the input is not within the valid pH range (0 to 14).\n \"\"\"\n \n if not isinstance(pH_value, (int, float, str)):\n raise TypeError(f\"Input type {type(pH_value)} is not valid. Must be int, float, or str.\")\n\n try:\n pH_value = float(pH_value)\n if not (0 <= pH_value <= 14):\n raise ValueError(\"pH value must be between 0 and 14.\")\n except ValueError as e:\n return f\"Invalid input: {e}\"\n\n # Classify the soil based on its pH value\n if pH_value < 7:\n return \"The soil is acidic.\"\n elif pH_value > 7:\n return \"The soil is alkaline.\"\n else:\n return \"The soil is neutral.\"\n\n# Example usage of the function\nprint(test_soil_pH(\"5.5\")) # Acidic soil\nprint(test_soil_pH(8.2)) # Alkaline soil\nprint(test_soil_pH(\"seven\")) # Invalid input\nprint(test_soil_pH(15)) # Valid input, out of range\nprint(test_soil_pH([7.5])) # Invalid type\n\nThe soil is acidic.\nThe soil is alkaline.\nInvalid input: could not convert string to float: 'seven'\nInvalid input: pH value must be between 0 and 14.\n\n\nTypeError: Input type <class 'list'> is not valid. Must be int, float, or str." + }, + { + "objectID": "basic_concepts/error_handling.html#explanation", + "href": "basic_concepts/error_handling.html#explanation", + "title": "27  Error handling", + "section": "Explanation", + "text": "Explanation\nData type check with isinstance: At the beginning of the function, we use isinstance() to check if the input is either an int, float, or str. The TypeError message includes the type of the wrong data type to inform the user about the nature of the error.\nConversion and range check: Inside the try block, the function attempts to convert the input to a float and then checks if it’s within the valid pH range, raising a ValueError if it’s out of range.\nHandling value error: The except block catches and returns a message for any ValueError." + }, + { + "objectID": "basic_concepts/numpy_module.html#element-wise-computations-using-numpy-arrays", + "href": "basic_concepts/numpy_module.html#element-wise-computations-using-numpy-arrays", + "title": "28  Numpy module", + "section": "Element-wise computations using Numpy arrays", + "text": "Element-wise computations using Numpy arrays\nWe will start this tutorial by learning about some of the limitations of traditional Python lists. Then, the power of Numpy element-wise (or vectorized) computations will become evident. As much as I like agronomic examples, for the first few exercises I will use some trivial arrays of numbers to keep it simple.\nVectorized or element-wise computations refer to operations that are performed on arrays (vectors, matrices) directly and simultaneously. Instead of processing elements one by one using loops, vectorized operations apply a single action to each element in the array without explicit iteration. This leads to more concise code and often improved performance. Both vectorized and element-wise temrs are correct and often are used interchangeably.\nLet’s begin by importing the Numpy module, so that we already have it for the entire tutorial.\n\n# Import numpy module\nimport numpy as np\nimport matplotlib.pyplot as plt # We will need this for some figures\n\n\nProduct of a regular list by a scalar\n\n# Create a list of elements\nA = [1,2,3,4]\n\n# Multiply the list by a scalar\nprint(A * 3)\n\n[1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]\n\n\n\n\nProduct of two regular lists with the same shape\n\n# Create list B, with the same size as A\nB = [5,6,7,8]\n\n# Multiply the two lists together. Heads up! This will not work!\nprint(A*B)\n\nTypeError: can't multiply sequence by non-int of type 'list'\n\n\nThe first operation repeats the list three times, which is probably not exactly what you were expecting. The second example results in an error. Now let’s import the Numpy module and try these operations again to see what happens.\n\n\nProduct of a Numpy array by a scalar\n\n# Create a list of elements\nA = np.array([1,2,3,4])\n\n# Check type of newly created Numpy array\nprint(type(A))\n\n# Multiply array by a scalar\nprint(A * 3)\n\n<class 'numpy.ndarray'>\n[ 3 6 9 12]\n\n\n\n\nProduct of two Numpy arrays with the same shape\nNotice that the operation occurs for corresponding elements of each array.\n\n# Re-define the previous B array as a numpy array\nB = np.array([5,6,7,8])\n\n# Multiply the two vectors\nprint(A * B)\n\n[ 5 12 21 32]\n\n\n\n\nOther operations with Numpy arrays\n\nprint(A * 3) # Vector times a scalar\nprint(A + B)\nprint(A - B)\nprint(A * B)\nprint(A / B)\nprint(np.sqrt(A**2 + B**2)) # Exponentiation Calculate hypotenuse of multiple rectangle triangle\nprint(A.sum())\nprint(B.sum())\n\n[ 3 6 9 12]\n[ 6 8 10 12]\n[-4 -4 -4 -4]\n[ 5 12 21 32]\n[0.2 0.33333333 0.42857143 0.5 ]\n[5.09901951 6.32455532 7.61577311 8.94427191]\n10\n26" + }, + { + "objectID": "basic_concepts/numpy_module.html#example-1-compute-soil-water-storage-for-a-single-field", + "href": "basic_concepts/numpy_module.html#example-1-compute-soil-water-storage-for-a-single-field", + "title": "28  Numpy module", + "section": "Example 1: Compute soil water storage for a single field", + "text": "Example 1: Compute soil water storage for a single field\nSoil water storage directly influences plant growth and crop yield since it is an essential processes for transpiration and nutrient uptake. In irrigated cropping systems, monitoring of soil water storage is important for irrigation scheduling and effective water management and conservation.\nAssume a field that has a soil profile with five horizons, each measuring: 10, 20, 30, 30, and 30 cm in thickness (so about 1.2 m depth). The volumetric water content for each horizon was determined by soil moisture sensors located at the center of each horizon, hence providing an average moisture value of: 0.350, 0.280, 0.255, 0.210, 0.137 cm^3/cm^3. Based on this information, compute the soil water storage for each soil horizon and the total for the entire soil profile. Recall that the volumetric water content represents the volume of water per unit volume of soil, so 0.350 cm^3/cm^3 is the same as 35% moisture by volume or by thickness of the soil horizon. How much irrigation water does a farmer need to add to reach a field capacity of 420 mm?\n\n# Define variables\ntheta_v = np.array([0.350, 0.280, 0.255, 0.210, 0.137]) # cm^3/cm^3\ndepths = np.array([10, 20, 30, 30, 30]) * 10 # horizons in mm\n\n# Compute water storage for each layer\nstorage_per_layer = theta_v * depths # mm of water per layer\nprint('Storage for each layer:', np.round(storage_per_layer,1))\n\n# Compute soil water storage for the entire profile\nprofile_storage = np.sum(storage_per_layer)\nprint(f'Total water storage in profile is: {profile_storage:.1f} mm')\n\nfield_capacity = 420 # mm\nirrigation_req = field_capacity - profile_storage\nprint(f'Required irrigation: {irrigation_req:.1f} mm')\n\nStorage for each layer: [35. 56. 76.5 63. 41.1]\nTotal water storage in profile is: 271.6 mm\nRequired irrigation: 148.4 mm\n\n\nSince the water storage for the entire soil profile is the weighted sum of the volumetric water content by the thickness of each layer, we can also use the dot product:\n\n\n\n\n\n\nNote\n\n\n\nLet’s review a few operations:\ndepths = np.array([10, 20, 30, 30, 30]) * 10 is the product of a vector and a scalar\nstorage_per_layer = theta_v * depths is a vector times another vector\n\n\n\n# dot product\nprint(np.dot(theta_v, depths), 'mm')\n\n271.6 mm" + }, + { + "objectID": "basic_concepts/numpy_module.html#example-2-compute-soil-water-storage-for-multiple-fields", + "href": "basic_concepts/numpy_module.html#example-2-compute-soil-water-storage-for-multiple-fields", + "title": "28  Numpy module", + "section": "Example 2: Compute soil water storage for multiple fields", + "text": "Example 2: Compute soil water storage for multiple fields\nNow imagine that we are managing three irrigated fields in the region? Assume that all the fields are nearby and have the same soil horizons, but farmers have different crops and irrigation strategies so they also have different soil moisture contents across the profile. What is the soil water storage of each field? How much water do we need to apply in each field to bring them back to field capacity?\n\n# Example soil moisture for the 5 horizons and three fields\n# The first field is the same as in the previous exercise\ntheta_v = np.array([[0.350, 0.280, 0.255, 0.210, 0.137],\n [0.250, 0.380, 0.355, 0.110, 0.250],\n [0.150, 0.180, 0.155, 0.110, 0.320]]) # cm^3/cm^3\n\n# Compute storage for all horizons\nstorage_per_layer = theta_v * depths\nprint(storage_per_layer)\n\n# Compute\nstorage_profiles = np.sum(storage_per_layer, axis=1) # axis=1 means add along columns\n\n# Alternatively we can do the dot product\n# storage_profiles = np.dot(theta_v, depths)\n\n# Show storage values\nprint(storage_profiles)\n\n# Irrigation requirements\nirrigation_req = field_capacity - storage_profiles\nprint('Irrigation for each field:', irrigation_req, 'mm')\n\n[[ 35. 56. 76.5 63. 41.1]\n [ 25. 76. 106.5 33. 75. ]\n [ 15. 36. 46.5 33. 96. ]]\n[271.6 315.5 226.5]\nIrrigation for each field: [148.4 104.5 193.5] mm\n\n\n\nExample 3: Determine the CEC of a soil\nThe cation exchange capacity (CEC, meq/100 g of soil) of a soil is determined by the nature and amount of clay minerals and organic matter. Compute the CEC of a soil that has 32% clay and 3% organic matter. The clay fraction is represented by 30% kaolinite, 50% montmorillonite, and 20% vermiculite. The CEC for clay minerals and organic matter can be found in most soil fertility textbooks.\n\n# Determine percentage of each clay mineral\n\nom = np.array([4]) # percent\nom_cec = np.array([200]) # meq/100 g\n\nclay = 32 * np.array([30, 50, 20])/100 # This is the % of each clay type\nclay_cec = np.array([10, 100, 140]) # meq/100 g\n\n# Merge the fractions and CEC together into a single aray\nall_fractions = np.concatenate((om, clay))/100 # percent to fraction\nall_cec = np.concatenate((om_cec, clay_cec))\n\nprint(all_fractions)\nprint(all_cec)\n\n[0.04 0.096 0.16 0.064]\n[200 10 100 140]\n\n\n\n# Compute soil CEC as the weighed-sum of its components\nsoil_cec = np.sum(all_cec * all_fractions)\nprint(f'The soil CEC is: {soil_cec:.1f} meq/100 g of soil')\n\nThe soil CEC is: 33.9 meq/100 g of soil\n\n\n\n\n\n\n\n\nNote\n\n\n\nLet’s review a few operations:\nnp.array([30, 50, 20])/100 is a vector divided by a scalar.\nall_cec * all_fractions is a vector times another vector\n\n\n\n\nCreate arrays with specific data types\n\n# An alternative by specifying the data type\nprint(np.array([1,2,3,4], dtype=\"int64\"))\nprint(np.array([1,2,3,4], dtype=\"float64\"))\n\n[1 2 3 4]\n[1. 2. 3. 4.]\n\n\n\n\n\n\n\n\nNote\n\n\n\nA common pitfall among beginners is to create a numpy array only using parentheses like this: array = np.array(1,2,3,4). This will not work.\n\n\n\n\nOperations with two-dimensional arrays\n\n# Define arrays\n# The values in M and v were arbitrarily selected\n# so that the operations result in round numbers for clarity. You can change them.\n\nM = np.array([ [10,2,1], [25,6,55] ]) # 2D matrix\nv = np.array([0.2, 0.5, 1]) # 1D vector\n\n\n# Access Numpy array shape and size properties\nprint(M.shape) # rows and columns\nprint(M.size) # Total number of elements\n\n(2, 3)\n6\n\n\n\n# Element-wise multiplication\nprint('Matrix by a scalar')\nprint(M*2)\n\nprint('Matrix by a vector with same number of columns')\nprint(M * v)\n\nprint('Matrix by matrix of the same size')\nprint(M*M)\n\nMatrix by a scalar\n[[ 20 4 2]\n [ 50 12 110]]\nMatrix by a vector with same number of columns\n[[ 2. 1. 1.]\n [ 5. 3. 55.]]\nMatrix by matrix of the same size\n[[ 100 4 1]\n [ 625 36 3025]]\n\n\n\n# Dot product (useful for linear algebra operations)\n# In this case the dot product is row-wise (sum of Matrix by a vector, see above)\nprint('Dot product operation')\nnp.dot(M,v)\n\nDot product operation\n\n\narray([ 4., 63.])\n\n\n\n\nReshape arrays\n\n# Reshape M array (original 2 rows 3 columns)\nprint(M)\n\n# Reshape to 3 rows and 2 columns\nprint(M.reshape(3,2))\n\n# Reshape to 1 row and 0 columns (1D array)\nprint(M.reshape(6,1))\nprint(M.reshape(6,1).shape) # Check the shape\n\n# Similar\n\n[[10 2 1]\n [25 6 55]]\n[[10 2]\n [ 1 25]\n [ 6 55]]\n[[10]\n [ 2]\n [ 1]\n [25]\n [ 6]\n [55]]\n(6, 1)" + }, + { + "objectID": "basic_concepts/numpy_module.html#numpy-boolean-operations", + "href": "basic_concepts/numpy_module.html#numpy-boolean-operations", + "title": "28  Numpy module", + "section": "Numpy boolean operations", + "text": "Numpy boolean operations\n\nexpr1 = np.array([1,2,3,4]) == 3\nprint(expr1)\n\nexpr2 = np.array([1,2,3,4]) == 2\nprint(expr2)\n\n[False False True False]\n[False True False False]\n\n\n\n# Elements in both vectors need to match to be considered True\nprint(expr1 & expr2) # print(expr1 and expr2)\n\n# It is sufficient with a single match in one of the vectors\nprint(expr1 | expr2) # print(expr1 or expr2)\n\n[False False False False]\n[False True True False]" + }, + { + "objectID": "basic_concepts/numpy_module.html#flattening", + "href": "basic_concepts/numpy_module.html#flattening", + "title": "28  Numpy module", + "section": "Flattening", + "text": "Flattening\nSometimes we want to serialize our 2D or 3D matrix into a long one-dimensional vector. This operation is called “flattening” and Numpy has a specific method to carry this operation that is called flattening and array.\n\n# Flatten two-dimensional array M\nM.flatten()\n\narray([10, 2, 1, 25, 6, 55])" + }, + { + "objectID": "basic_concepts/numpy_module.html#use-the-numpy-random-module-to-create-a-random-image", + "href": "basic_concepts/numpy_module.html#use-the-numpy-random-module-to-create-a-random-image", + "title": "28  Numpy module", + "section": "Use the Numpy random module to create a random image", + "text": "Use the Numpy random module to create a random image\nTo show some of the power of working with Numpy arrays we will create a random image. Images in the RGB (red-green-blue) color space are represented by three matrices that encode the color of each pixel. Colors are represented with an integer between 0 and 255. This is called an 8-bit integer, and there are only 2^8 = 256 possible integers. Because each image has three bands (one for red, one for green, and one for blue) there is a total of 17 million (256x256x256 or 256^3) possible colors for each pixel.\n\n# Define image size (keep it small so that you can see each pixel)\nrows = 20\ncols = 30\n\n# Set seed for reproducibility.\n# Everyone will obtain the same random numbers\nnp.random.seed(1) \n\n# Create image bands\n# uint8 means unsigned interger of 8-bits\nR = np.random.randint(0, 255, (rows, cols), dtype='uint8') \nG = np.random.randint(0, 255, (rows, cols), dtype='uint8')\nB = np.random.randint(0, 255, (rows, cols), dtype='uint8')\n\n# Stack image bands (axis=2 means along the third dimension, or on top of each other)\nRGB = np.stack( (R,G,B), axis=2)\nprint('Image size:', RGB.shape) # Shape of the RGB variable\n\n# Display image using the matplotlib library (we imported this at the top)\nplt.figure(figsize=(8,8))\nplt.imshow(RGB)\nplt.axis('off') # Mute this line to see what the image looks like without it.\nplt.show()\n\nImage size: (20, 30, 3)" + }, + { + "objectID": "basic_concepts/numpy_module.html#numpy-handy-functions", + "href": "basic_concepts/numpy_module.html#numpy-handy-functions", + "title": "28  Numpy module", + "section": "Numpy handy functions", + "text": "Numpy handy functions\n\n# Generate a range of integers\n# np.arange(start,stop,step)\nprint('range()')\nprint(np.arange(0,100,10))\n\n# Generate linear range\n# numpy.linspace(start, stop, num=50, endpoint=True)\nprint('')\nprint('linspace()')\nprint(np.linspace(0, 10, 5))\n\n# Array of zeros\nprint('')\nprint('zeros()')\nprint(np.zeros([5,3]))\n\n# Array of ones\nprint('')\nprint('ones()')\nprint(np.ones([4,3]))\n\n# Array of NaN values\nprint('')\nprint('full()')\nprint(np.full([4,3], np.nan)) # This also worksprint(np.ones([4,3])*np.nan)\n\n# Meshgrid (first create 1D vectors, then create a 2D mesh)\nN = 5\nlat = np.linspace(36, 40, N)\nlon = np.linspace(-102, -98, N)\nLAT,LON = np.meshgrid(lat,lon)\nprint('')\nprint('Grid of latitudes')\nprint(LAT)\nprint('')\nprint('Grid of longitudes')\nprint(LON)\n\nrange()\n[ 0 10 20 30 40 50 60 70 80 90]\n\nlinspace()\n[ 0. 2.5 5. 7.5 10. ]\n\nzeros()\n[[0. 0. 0.]\n [0. 0. 0.]\n [0. 0. 0.]\n [0. 0. 0.]\n [0. 0. 0.]]\n\nones()\n[[1. 1. 1.]\n [1. 1. 1.]\n [1. 1. 1.]\n [1. 1. 1.]]\n\nfull()\n[[nan nan nan]\n [nan nan nan]\n [nan nan nan]\n [nan nan nan]]\n\nGrid of latitudes\n[[36. 37. 38. 39. 40.]\n [36. 37. 38. 39. 40.]\n [36. 37. 38. 39. 40.]\n [36. 37. 38. 39. 40.]\n [36. 37. 38. 39. 40.]]\n\nGrid of longitudes\n[[-102. -102. -102. -102. -102.]\n [-101. -101. -101. -101. -101.]\n [-100. -100. -100. -100. -100.]\n [ -99. -99. -99. -99. -99.]\n [ -98. -98. -98. -98. -98.]]" + }, + { + "objectID": "basic_concepts/numpy_module.html#create-a-noisy-wave", + "href": "basic_concepts/numpy_module.html#create-a-noisy-wave", + "title": "28  Numpy module", + "section": "Create a noisy wave", + "text": "Create a noisy wave\nWith Numpy we can easily implement models, create timeseries, add noise, and perform trigonometric operations. In this example we will create a synthetic timeseries of air temperature using a cosine wave. To make this more realistic we will also add some noise.\n\n# Set random seed for reproducibility\nnp.random.seed(1) \n\n# Define wave inputs\nT_avg = 15 # Annual average in Celsius\nA = 10 # Annual amplitude [Celsius]\ndoy = np.arange(1,366) # Vector of days of the year\n\n# Generate x and y axis\nx = 2 * np.pi * doy/365 # Convert doy into pi-radians \ny = T_avg - A*np.cos(x) # Sine wave\n\n# Add random noise\nnoise = np.random.normal(0, 3, x.size) # White noise having zero mean\ny_noisy = y + noise\n\n# Visualize wave using Matplotlib\nplt.figure(figsize=(6,3))\nplt.title('Noisy temperature timeseries')\nplt.plot(doy, y_noisy, '-k', label=\"Wave with noise\")\nplt.plot(doy, y, '-r', linewidth=2, label=\"Wave without noise\")\nplt.xlabel('Day of the Year', size=12)\nplt.ylabel('Air Temperature (Celsius)', size=12)\nplt.legend()\nplt.show()\n\n\n\n\n\nDescriptive stats\nTo finish our tutorial, let’s inpsect Numpy methods to obtain some descriptive statistics for the wave we created earlier.\n\n# Descriptive stats\nprint('Mean:', y.mean()) # Arithmetic average\nprint('Standard deviation:', y.std()) # Standard deviation\nprint('Variance:', y.var()) # Variance\nprint('Median:', np.median(y)) # Median\nprint('Minimum:', y.min()) # Minimum\nprint('Maximum:', y.max()) # Maximum\nprint('Index of minimum:', y.argmin()) # Position of minimum value\nprint('Index of maximum:', y.argmax()) # Position of maximum value\nprint('50th percentile:', np.percentile(y, 50)) # 50th percentile (should equal to the median)\nprint('5th and 95th percentiles:', np.percentile(y, [5,95])) # 5th and 95th percentile\n\nMean: 15.0\nStandard deviation: 7.0710678118654755\nVariance: 50.0\nMedian: 15.000000000000007\nMinimum: 5.000092602638098\nMaximum: 24.999907397361902\nIndex of minimum: 273\nIndex of maximum: 90\n50th percentile: 15.000000000000007\n5th and 95th percentiles: [ 5.12930816 24.87069184]" + }, + { + "objectID": "basic_concepts/numpy_module.html#reference", + "href": "basic_concepts/numpy_module.html#reference", + "title": "28  Numpy module", + "section": "Reference", + "text": "Reference\nWalt, S.V.D., Colbert, S.C. and Varoquaux, G., 2011. The NumPy array: a structure for efficient numerical computation. Computing in science & engineering, 13(2), pp.22-30." + }, + { + "objectID": "basic_concepts/pandas_module.html#create-dataframe-from-existing-variable", + "href": "basic_concepts/pandas_module.html#create-dataframe-from-existing-variable", + "title": "29  Pandas module", + "section": "Create DataFrame from existing variable", + "text": "Create DataFrame from existing variable\nAfter importing the module we have two possible directions. We import data from a file or we convert an existing variable into a Pandas DataFrame. Here we will create a simple DatFrame to learn the basics. This way we will be able to display the result of our operations without worrying about extensive datasets.\nLet’s create a dictionary with some weather data and missing values (represented by -9999).\n\n# Create dictionary with some weather data\ndata = {'timestamp': ['1/1/2000','2/1/2000','3/1/2000','4/1/2000','5/1/2000'], \n 'wind_speed': [2.2, 3.2, -9999.0, 4.1, 2.9], \n 'wind_direction': ['E', 'NW', 'NW', 'N', 'S'],\n 'precipitation': [0, 18, 25, 2, 0]}\n\nThe next step consists of converting the dictionary into a Pandas DataFrame. This is straight forward using the DataFrame method of the Pandas module: pd.DataFrame()\n\n# Convert dictionary into DataFrame\ndf = pd.DataFrame(data)\ndf.head()\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\n\n\n\n\n0\n1/1/2000\n2.2\nE\n0\n\n\n1\n2/1/2000\n3.2\nNW\n18\n\n\n2\n3/1/2000\n-9999.0\nNW\n25\n\n\n3\n4/1/2000\n4.1\nN\n2\n\n\n4\n5/1/2000\n2.9\nS\n0\n\n\n\n\n\n\n\nThe above DataFrame has the following components:\n\nheader row containing column names\nindex (the left-most column with numbers from 0 to 4) is equivalent to a row name.\nEach column has data of the same type.\n\n\n# By default, values in Pandas series are Numpy arrays\nprint(df[\"wind_speed\"].values)\nprint(type(df[\"wind_speed\"].values))\n\n[ 2.200e+00 3.200e+00 -9.999e+03 4.100e+00 2.900e+00]\n<class 'numpy.ndarray'>" + }, + { + "objectID": "basic_concepts/pandas_module.html#basic-methods-and-properties", + "href": "basic_concepts/pandas_module.html#basic-methods-and-properties", + "title": "29  Pandas module", + "section": "Basic methods and properties", + "text": "Basic methods and properties\nPandas DataFrame has dedicated functions to display a limited number of heading and tailing rows.\n\ndf.head(3) # First three rows\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\n\n\n\n\n0\n1/1/2000\n2.2\nE\n0\n\n\n1\n2/1/2000\n3.2\nNW\n18\n\n\n2\n3/1/2000\n-9999.0\nNW\n25\n\n\n\n\n\n\n\n\ndf.tail(3) # Last three rows\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\n\n\n\n\n2\n3/1/2000\n-9999.0\nNW\n25\n\n\n3\n4/1/2000\n4.1\nN\n2\n\n\n4\n5/1/2000\n2.9\nS\n0\n\n\n\n\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nTo display the DataFrame content simply use the head() and tail() methods. As an alternative you can use the print() function or type the name of the DataFrame and press ctrl + Enter. Note that by default Jupyter Lab highlights rows when using the head() or tail() methods.\n\n\nTo start exploring and analyzing our dataset it is often handy to know the column names.\n\n# Display column names\ndf.columns\n\nIndex(['timestamp', 'wind_speed', 'wind_direction', 'precipitation'], dtype='object')\n\n\n\n# Total number of elements\ndf.size\n\n20\n\n\n\n# Number of rows and columns\ndf.shape\n\n(5, 4)\n\n\n\n# Data type of each column\ndf.dtypes\n\ntimestamp object\nwind_speed float64\nwind_direction object\nprecipitation int64\ndtype: object" + }, + { + "objectID": "basic_concepts/pandas_module.html#convert-strings-to-datetime", + "href": "basic_concepts/pandas_module.html#convert-strings-to-datetime", + "title": "29  Pandas module", + "section": "Convert strings to datetime", + "text": "Convert strings to datetime\n\n# Convert dates in string format to Pandas datetime format\n# %d = day in format 00 days\n# %m = month in format 00 months\n# %Y = full year\n\ndf[\"timestamp\"] = pd.to_datetime(df[\"timestamp\"], format=\"%d/%m/%Y\")\ndf.head()\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\n\n\n\n\n0\n2000-01-01\n2.2\nE\n0\n\n\n1\n2000-01-02\n3.2\nNW\n18\n\n\n2\n2000-01-03\n-9999.0\nNW\n25\n\n\n3\n2000-01-04\n4.1\nN\n2\n\n\n4\n2000-01-05\n2.9\nS\n0\n\n\n\n\n\n\n\n\n# The `timestamp` column has changed to datetime format\ndf.dtypes\n\ntimestamp datetime64[ns]\nwind_speed float64\nwind_direction object\nprecipitation int64\ndtype: object" + }, + { + "objectID": "basic_concepts/pandas_module.html#extract-information-from-the-timestamp", + "href": "basic_concepts/pandas_module.html#extract-information-from-the-timestamp", + "title": "29  Pandas module", + "section": "Extract information from the timestamp", + "text": "Extract information from the timestamp\nHaving specific information like day of the year, month, or weeks in a separate column can be useful to help us aggregate values. For instance, to compute the monthly mean air temperature we need to know in what month each temperature observations was recorded.\nFor this we will use the dt submodule within Pandas.\n\n# Get the day of the year\ndf[\"doy\"] = df[\"timestamp\"].dt.dayofyear\ndf.head()\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n-9999.0\nNW\n25\n3\n\n\n3\n2000-01-04\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n2.9\nS\n0\n5\n\n\n\n\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nThe new column was placed at the end of the DataFrame. This the default when creating a new column.\n\n\nIn the next example we use the insert() method to add the new column in a specific location. Typically, for date components it helps to have the columns close to the datetime column.\n\n# Get month from timstamp and create new column\n\n#.insert(positionOfNewColumn, nameOfNewColumn, dataOfNewColumn)\n\ndf.insert(1,'month',df[\"timestamp\"].dt.month)\ndf.head()\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\n-9999.0\nNW\n25\n3\n\n\n3\n2000-01-04\n1\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n1\n2.9\nS\n0\n5\n\n\n\n\n\n\n\n\n\n\n\n\n\nWarning\n\n\n\nRe-running the previous cell will trigger an error since Pandas cannot have two columns with the same name." + }, + { + "objectID": "basic_concepts/pandas_module.html#missing-values", + "href": "basic_concepts/pandas_module.html#missing-values", + "title": "29  Pandas module", + "section": "Missing values", + "text": "Missing values\nOne of the most common operations when working with data is handling missing values. Almost every dataset has missing data and there is no universal way of denoting missing values. Most common placeholders are: NaN, NA, -99, -9999, M, missing, etc. To find out more about how missing data is represented in your dataset always read associated metadata. Some of these placeholders are automatically identified by Pandas as missing values and are represented as NaN values.\nPandas methods can deal with missing data, meaning that it is not necessary to always replace missing values in order to make computations. We just need to ensure that missing values are represented as NaN values.\nThe fillna() and interpolate() methods can help us replace missing values with an approximate value using neighboring points.\nTo replace missing values in our current dataset, we will follow these steps:\n\nIdentify the cells with -9999 values. Output will be a boolean DataFrame having the same dimensions as df.\nReplace -9999 with NaN values from the Numpy module.\nCheck our work using the isna() method\n\n\n\n\n\n\n\nNote\n\n\n\nMissing values represented as np.nan are actually of type float.\n\n\n\n# Print data type of NaN values from Numpy\ntype(np.nan)\n\nfloat\n\n\n\n# Step 1: find -9999 values across the entire dataframe\n\nidx_missing = df.isin([-9999]) # or idx_missing = df == -9999\nidx_missing\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n1\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n2\nFalse\nFalse\nTrue\nFalse\nFalse\nFalse\n\n\n3\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n4\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n\n\n\n\n\n\n# Find missing vlaues in only one column\ndf[\"wind_speed\"].isin([-9999]) # or df[\"wind_speed\"] == -99999\n\n0 False\n1 False\n2 True\n3 False\n4 False\nName: wind_speed, dtype: bool\n\n\nUsing the isin() method we can place multiple placeholders denoting missing data, as opposed to the boolean statement that would require multiple or statements.\n\n# Step 2: Replace missing values with NaN\n\ndf[idx_missing] = np.nan\ndf\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\nNaN\nNW\n25\n3\n\n\n3\n2000-01-04\n1\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n1\n2.9\nS\n0\n5\n\n\n\n\n\n\n\n\n# Step 3: Check our work\ndf.isna()\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n1\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n2\nFalse\nFalse\nTrue\nFalse\nFalse\nFalse\n\n\n3\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse\n\n\n4\nFalse\nFalse\nFalse\nFalse\nFalse\nFalse" + }, + { + "objectID": "basic_concepts/pandas_module.html#quick-statistics", + "href": "basic_concepts/pandas_module.html#quick-statistics", + "title": "29  Pandas module", + "section": "Quick statistics", + "text": "Quick statistics\nDataFrames have a variety of methods to calculate simple statistics. To obtain an overall summary we can use the describe() method.\n\n# Summary stats for all columns\nprint(df.describe())\n\n timestamp month wind_speed precipitation doy\ncount 5 5.0 4.000000 5.0000 5.000000\nmean 2000-01-03 00:00:00 1.0 3.100000 9.0000 3.000000\nmin 2000-01-01 00:00:00 1.0 2.200000 0.0000 1.000000\n25% 2000-01-02 00:00:00 1.0 2.725000 0.0000 2.000000\n50% 2000-01-03 00:00:00 1.0 3.050000 2.0000 3.000000\n75% 2000-01-04 00:00:00 1.0 3.425000 18.0000 4.000000\nmax 2000-01-05 00:00:00 1.0 4.100000 25.0000 5.000000\nstd NaN 0.0 0.787401 11.7047 1.581139\n\n\n\n# Metric ignoring NaN values\nprint(df[\"wind_speed\"].max()) # Maximum value for each column\nprint(df[\"wind_speed\"].mean()) # Average value for each column\nprint(df[\"wind_speed\"].min()) # Minimum value for each column\nprint(df[\"wind_speed\"].std()) # Standard deviation value for each column\nprint(df[\"wind_speed\"].var()) # Variance value for each column\nprint(df[\"wind_speed\"].median()) # Variance value for each column\nprint(df[\"wind_speed\"].quantile(0.95))\n\n4.1\n3.1\n2.2\n0.7874007874011809\n0.6199999999999997\n3.05\n3.9649999999999994\n\n\n\n# Cumulative sum. Useful to compute cumulative precipitation\nprint(df.precipitation.cumsum())\n\n0 0\n1 18\n2 43\n3 45\n4 45\nName: precipitation, dtype: int64\n\n\n\n# Unique values. Useful to compute unique wind directions\nprint(df.wind_direction.unique())\n\n['E' 'NW' 'N' 'S']" + }, + { + "objectID": "basic_concepts/pandas_module.html#indexing-and-slicing", + "href": "basic_concepts/pandas_module.html#indexing-and-slicing", + "title": "29  Pandas module", + "section": "Indexing and slicing", + "text": "Indexing and slicing\nTo start making computations with need to access the data insde the Pandas DataFrame. Indexing and slicing are useful operations to select portions of data by calling specific rows, columns, or a combination of both. The index operator [] is primarily intended to be used with column labels (e.g. df[columnName]), however, it can also handle row slices (e.g. df[rows]). A common notation useful to understand how the slicing works is as follows:\n\nSelect rows\n\n# First three rows\ndf[0:3]\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\nNaN\nNW\n25\n3\n\n\n\n\n\n\n\n\n\nSelect columns\nWe can call individual columns using the dot or bracket notation. Note that in option 2 there is no . between df and ['windSpeed']\n\ndf['wind_speed'] # Option 1\n# df.wind_speed # Option 2\n\n0 2.2\n1 3.2\n2 NaN\n3 4.1\n4 2.9\nName: wind_speed, dtype: float64\n\n\nTo pass more than one row of column you will need to group them in a list.\n\n# Select multiple columns at once\ndf[['wind_speed','wind_direction']]\n\n\n\n\n\n\n\n\nwind_speed\nwind_direction\n\n\n\n\n0\n2.2\nE\n\n\n1\n3.2\nNW\n\n\n2\nNaN\nNW\n\n\n3\n4.1\nN\n\n\n4\n2.9\nS\n\n\n\n\n\n\n\nA common mistake when slicing multiple columns is to forget grouping column names into a list, so the following command will not work:\ndf['wind_speed','wind_direction']\n\n\nUsing iloc method\niloc: Integer-location. iloc gets rows (or columns) at specific indexes. It only takes integers as input. Exclusive of its endpoint\n\n# Top 3 rows and columns 1 and 2\ndf.iloc[0:3,[1,2]]\n\n\n\n\n\n\n\n\nmonth\nwind_speed\n\n\n\n\n0\n1\n2.2\n\n\n1\n1\n3.2\n\n\n2\n1\nNaN\n\n\n\n\n\n\n\n\n# Top 2 rows and all columns\ndf.iloc[0:2,:] # Same as: df.iloc[0:2]\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n\n\n\n\n\nAlthough a bit more verbose and perhaps less pythonic, sometimes it is better to specify all the columns using the colon : character. In my opinion this notation is more explicit and clearly states the rows and columns of the slicing operation. For instance, df.iloc[0:2,:] is more explicit than df.iloc[0:2].\n\n\nUsing loc method\nloc gets rows (or columns) with specific labels. Inclusive of its endpoint\n\n# Select multiple rows and columns at once using the loc method\ndf.loc[0:2,['wind_speed','wind_direction']]\n\n\n\n\n\n\n\n\nwind_speed\nwind_direction\n\n\n\n\n0\n2.2\nE\n\n\n1\n3.2\nNW\n\n\n2\nNaN\nNW\n\n\n\n\n\n\n\n\n# Some rows and all columns\ndf.loc[0:1,:]\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n\n\n\n\n\n\n# First three elements of a single column\ndf.loc[0:2,'wind_speed'] \n\n0 2.2\n1 3.2\n2 NaN\nName: wind_speed, dtype: float64\n\n\n\n# First three elements of multiple columns\ndf.loc[0:2,['wind_speed','wind_direction']]\n\n\n\n\n\n\n\n\nwind_speed\nwind_direction\n\n\n\n\n0\n2.2\nE\n\n\n1\n3.2\nNW\n\n\n2\nNaN\nNW\n\n\n\n\n\n\n\nThese statements will not work with loc:\ndf.loc[0:2,0:1]\ndf.loc[[0:2],[0:1]]" + }, + { + "objectID": "basic_concepts/pandas_module.html#filter-data-using-boolean-indexing", + "href": "basic_concepts/pandas_module.html#filter-data-using-boolean-indexing", + "title": "29  Pandas module", + "section": "Filter data using boolean indexing", + "text": "Filter data using boolean indexing\nBoolean indexing (a.k.a. logical indexing) consists of creating an array with True/False values as a result of one or more conditional statements that can be use to select data that meet the specified condition.\nLet’s select the data across all columns for days that have wind speed greater than 3 meters per second. We will first select the rows of df.wind_speed that are greater than 3 m/s, and then we will use the resulting boolean to slice the DataFrame.\n\nidx = df['wind_speed'] > 3 # Rows in which the wind speed is greater than \nidx # Let's inspect the idx variable.\n\n0 False\n1 True\n2 False\n3 True\n4 False\nName: wind_speed, dtype: bool\n\n\n\n# Now let's apply the boolean variable to the dataframe\ndf[idx]\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n3\n2000-01-04\n1\n4.1\nN\n2\n4\n\n\n\n\n\n\n\n\n# We can also apply the boolean to specific columns\ndf.loc[idx,'wind_direction']\n\n1 NW\n3 N\nName: wind_direction, dtype: object\n\n\nIt’s also possible to write the previous command as a single line of code. This is fine, but sometimes nesting too many conditions can create commands that are hard to read and understand. To avoid this problem, storing the boolean in a new variable makes things a lot easier to read and re-use.\n\n# Same in a single line of code\ndf.loc[df['wind_speed'] > 3,'wind_direction']\n\n1 NW\n3 N\nName: wind_direction, dtype: object\n\n\nAnother popular way of filtering is to check whether an element or group of elements are within a set. Let’s check whether January 1 and January 2 are in the DataFrame.\n\nidx_doy = df['doy'].isin([1,2])\nidx_doy\n\n0 True\n1 True\n2 False\n3 False\n4 False\nName: doy, dtype: bool\n\n\n\n# Select all columns for the selected days of the year\ndf.loc[idx_doy,:]\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2" + }, + { + "objectID": "basic_concepts/pandas_module.html#pandas-custom-date-range", + "href": "basic_concepts/pandas_module.html#pandas-custom-date-range", + "title": "29  Pandas module", + "section": "Pandas custom date range", + "text": "Pandas custom date range\nMost datasets collected over a period of time include timestamps, but in case the timestamps are missing or you need to create a range of dates during your analysis, Pandas has the date_range() method to create a sequence of timestamps.\n\nsubset_dates = pd.date_range('20000102', periods=2, freq='D') # Used df.shape[0] to find the total number of rows\nsubset_dates\n\nDatetimeIndex(['2000-01-02', '2000-01-03'], dtype='datetime64[ns]', freq='D')\n\n\n\n# The same to generate months\npd.date_range('20200101', periods=df.shape[0], freq='M') # Specify the frequency to months\n\nDatetimeIndex(['2020-01-31', '2020-02-29', '2020-03-31', '2020-04-30',\n '2020-05-31'],\n dtype='datetime64[ns]', freq='M')" + }, + { + "objectID": "basic_concepts/pandas_module.html#select-range-of-dates-with-boolean-indexing", + "href": "basic_concepts/pandas_module.html#select-range-of-dates-with-boolean-indexing", + "title": "29  Pandas module", + "section": "Select range of dates with boolean indexing", + "text": "Select range of dates with boolean indexing\nNow that we covered both boolean indexing and pandas dates we can use these concepts to select data from a specific window of time. This is a pretty common operation when trying to select a subset of the entire DataFrame by a specific date range.\n\n# Generate boolean for rows that match the subset of dates generated earlier\nidx_subset = df[\"timestamp\"].isin(subset_dates)\nidx_subset\n\n0 False\n1 True\n2 True\n3 False\n4 False\nName: timestamp, dtype: bool\n\n\n\n# Generate a new DataFrame using only the rows with matching dates\ndf_subset = df.loc[idx_subset]\ndf_subset\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\nNaN\nNW\n25\n3\n\n\n\n\n\n\n\n\n# It isn't always necessary to generate a new DataFrame\n# So you can access a specific column like this\ndf.loc[idx_subset,\"precipitation\"]\n\n1 18\n2 25\nName: precipitation, dtype: int64" + }, + { + "objectID": "basic_concepts/pandas_module.html#add-and-remove-columns", + "href": "basic_concepts/pandas_module.html#add-and-remove-columns", + "title": "29  Pandas module", + "section": "Add and remove columns", + "text": "Add and remove columns\nThe insert() and drop() methods allow us to add or remove columns to/from the DataFrame. The most common use of these functions is as follows:\ndf.insert(index_of_new_column, name_of_new_column, data_of_new_column)\ndf.drop(name_of_column_to_be_removed)\n\n# Add new column at a specific location\ndf.insert(2, 'air_temperature', [25.4, 26, 27.1, 28.9, 30.2]) # Similar to: df['dates'] = dates\ndf\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n25.4\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n26.0\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\n27.1\nNaN\nNW\n25\n3\n\n\n3\n2000-01-04\n1\n28.9\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n1\n30.2\n2.9\nS\n0\n5\n\n\n\n\n\n\n\n\n# Remove specific column\ndf.drop(columns=['air_temperature'])\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\nNaN\nNW\n25\n3\n\n\n3\n2000-01-04\n1\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n1\n2.9\nS\n0\n5" + }, + { + "objectID": "basic_concepts/pandas_module.html#reset-dataframe-index", + "href": "basic_concepts/pandas_module.html#reset-dataframe-index", + "title": "29  Pandas module", + "section": "Reset DataFrame index", + "text": "Reset DataFrame index\n\n# Replace the index by a variables of our choice\ndf.set_index('timestamp')\n\n\n\n\n\n\n\n\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\ntimestamp\n\n\n\n\n\n\n\n\n\n\n2000-01-01\n1\n25.4\n2.2\nE\n0\n1\n\n\n2000-01-02\n1\n26.0\n3.2\nNW\n18\n2\n\n\n2000-01-03\n1\n27.1\nNaN\nNW\n25\n3\n\n\n2000-01-04\n1\n28.9\n4.1\nN\n2\n4\n\n\n2000-01-05\n1\n30.2\n2.9\nS\n0\n5\n\n\n\n\n\n\n\n\n# Reset the index (see that 'doy' goes back to the end of the DataFrame again)\ndf.reset_index(0, drop=True)\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1\n25.4\n2.2\nE\n0\n1\n\n\n1\n2000-01-02\n1\n26.0\n3.2\nNW\n18\n2\n\n\n2\n2000-01-03\n1\n27.1\nNaN\nNW\n25\n3\n\n\n3\n2000-01-04\n1\n28.9\n4.1\nN\n2\n4\n\n\n4\n2000-01-05\n1\n30.2\n2.9\nS\n0\n5" + }, + { + "objectID": "basic_concepts/pandas_module.html#merge-two-dataframes", + "href": "basic_concepts/pandas_module.html#merge-two-dataframes", + "title": "29  Pandas module", + "section": "Merge two dataframes", + "text": "Merge two dataframes\n\n# Create a new DataFrame (follows dates)\n\n# Dictionary\ndata2 = {'timestamp': ['6/1/2000','7/1/2000','8/1/2000','9/1/2000','10/1/2000'], \n 'wind_speed': [4.3, 2.1, 0.5, 2.7, 1.9], \n 'wind_direction': ['N', 'N', 'SW', 'E', 'NW'],\n 'precipitation': [0, 0, 0, 25, 0]}\n\n# Dcitionary to DataFrame\ndf2 = pd.DataFrame(data2)\n\n# Convert strings to pandas datetime\ndf2[\"timestamp\"] = pd.to_datetime(df2[\"timestamp\"], format=\"%d/%m/%Y\") \n\ndf2.head()\n\n\n\n\n\n\n\n\ntimestamp\nwind_speed\nwind_direction\nprecipitation\n\n\n\n\n0\n2000-01-06\n4.3\nN\n0\n\n\n1\n2000-01-07\n2.1\nN\n0\n\n\n2\n2000-01-08\n0.5\nSW\n0\n\n\n3\n2000-01-09\n2.7\nE\n25\n\n\n4\n2000-01-10\n1.9\nNW\n0\n\n\n\n\n\n\n\n\n\n\n\n\n\nWarning\n\n\n\nNot using the format=\"%d/%m/%y\" in the previous cell results in the wrong datetime conversion. It is always recommended to specify the format.\n\n\n\n# Merge both Dataframes by applying a union of keys from both frames (how='outer' option)\ndf_merged = pd.merge(df, df2, how='outer')\ndf_merged\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\n\n\n\n\n0\n2000-01-01\n1.0\n25.4\n2.2\nE\n0\n1.0\n\n\n1\n2000-01-02\n1.0\n26.0\n3.2\nNW\n18\n2.0\n\n\n2\n2000-01-03\n1.0\n27.1\nNaN\nNW\n25\n3.0\n\n\n3\n2000-01-04\n1.0\n28.9\n4.1\nN\n2\n4.0\n\n\n4\n2000-01-05\n1.0\n30.2\n2.9\nS\n0\n5.0\n\n\n5\n2000-01-06\nNaN\nNaN\n4.3\nN\n0\nNaN\n\n\n6\n2000-01-07\nNaN\nNaN\n2.1\nN\n0\nNaN\n\n\n7\n2000-01-08\nNaN\nNaN\n0.5\nSW\n0\nNaN\n\n\n8\n2000-01-09\nNaN\nNaN\n2.7\nE\n25\nNaN\n\n\n9\n2000-01-10\nNaN\nNaN\n1.9\nNW\n0\nNaN\n\n\n\n\n\n\n\n\nNote how NaN values were assigned to variables not present in the new DataFrame\n\n\n# Create another DataFrame with more limited data. Values every other day\ndata3 = {'timestamp': ['1/1/2000','3/1/2000','5/1/2000','7/1/2000','9/1/2000'], \n 'pressure': [980, 987, 985, 991, 990]} # Pressure in millibars\n\ndf3 = pd.DataFrame(data3)\ndf3[\"timestamp\"] = pd.to_datetime(df3[\"timestamp\"], format=\"%d/%m/%Y\")\ndf3.head()\n\n\n\n\n\n\n\n\ntimestamp\npressure\n\n\n\n\n0\n2000-01-01\n980\n\n\n1\n2000-01-03\n987\n\n\n2\n2000-01-05\n985\n\n\n3\n2000-01-07\n991\n\n\n4\n2000-01-09\n990\n\n\n\n\n\n\n\n\n# Only the matching rows will be merged\ndf_merged.merge(df3, on=\"timestamp\")\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\npressure\n\n\n\n\n0\n2000-01-01\n1.0\n25.4\n2.2\nE\n0\n1.0\n980\n\n\n1\n2000-01-03\n1.0\n27.1\nNaN\nNW\n25\n3.0\n987\n\n\n2\n2000-01-05\n1.0\n30.2\n2.9\nS\n0\n5.0\n985\n\n\n3\n2000-01-07\nNaN\nNaN\n2.1\nN\n0\nNaN\n991\n\n\n4\n2000-01-09\nNaN\nNaN\n2.7\nE\n25\nNaN\n990\n\n\n\n\n\n\n\n\n# Only add values from the new, more sporadic, variable where there is a match.\ndf_merged.merge(df3, how=\"left\")\n\n\n\n\n\n\n\n\ntimestamp\nmonth\nair_temperature\nwind_speed\nwind_direction\nprecipitation\ndoy\npressure\n\n\n\n\n0\n2000-01-01\n1.0\n25.4\n2.2\nE\n0\n1.0\n980.0\n\n\n1\n2000-01-02\n1.0\n26.0\n3.2\nNW\n18\n2.0\nNaN\n\n\n2\n2000-01-03\n1.0\n27.1\nNaN\nNW\n25\n3.0\n987.0\n\n\n3\n2000-01-04\n1.0\n28.9\n4.1\nN\n2\n4.0\nNaN\n\n\n4\n2000-01-05\n1.0\n30.2\n2.9\nS\n0\n5.0\n985.0\n\n\n5\n2000-01-06\nNaN\nNaN\n4.3\nN\n0\nNaN\nNaN\n\n\n6\n2000-01-07\nNaN\nNaN\n2.1\nN\n0\nNaN\n991.0\n\n\n7\n2000-01-08\nNaN\nNaN\n0.5\nSW\n0\nNaN\nNaN\n\n\n8\n2000-01-09\nNaN\nNaN\n2.7\nE\n25\nNaN\n990.0\n\n\n9\n2000-01-10\nNaN\nNaN\n1.9\nNW\n0\nNaN\nNaN" + }, + { + "objectID": "basic_concepts/pandas_module.html#operations-with-real-dataset", + "href": "basic_concepts/pandas_module.html#operations-with-real-dataset", + "title": "29  Pandas module", + "section": "Operations with real dataset", + "text": "Operations with real dataset\n\n# Read CSV file\ndata_url = \"../datasets/ok_mesonet_8_apr_2019.csv\"\ndf = pd.read_csv(data_url)\ndf.head(5)\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\n\n\n\n\n\n\n\n\n\n\n\n\n1\nADAX\nAda\nOK\n34.80\n-96.67\n2019\n4\n15\n15\n20\n...\n40\n\n\nS\n12\n20\n1011.13\n78\n48\n\n\n\n2\nALTU\nAltus\nOK\n34.59\n-99.34\n2019\n4\n15\n15\n20\n...\n39\n\n82\nSSW\n19\n26\n1007.86\n82\n45\n\n\n\n3\nALV2\nAlva\nOK\n36.71\n-98.71\n2019\n4\n15\n15\n20\n...\n32\n\n82\nS\n20\n26\n1004.65\n84\n40\n\n\n\n4\nANT2\nAntlers\nOK\n34.25\n-95.67\n2019\n4\n15\n15\n20\n...\n35\n\n\nS\n11\n20\n1013.64\n78\n38\n\n\n\n\n\n5 rows × 22 columns\n\n\n\n\n\n\n\n\n\nWarning\n\n\n\nSome columns seem to have empty cells. Ideally we would like to see these cells filled with NaN values. Something looks fishy. Let’s inspect some of these cells.\n\n\n\n# Print one the cells to see what's in there\ndf.loc[0,'RAIN']\n\n' '\n\n\nIn the inspected cell we found a string with a single space in it. Now we can use the replace() method to substitute these strings for NaN values.\n\n\n\n\n\n\nNote\n\n\n\nIn Pandas, the inplace=True argument is used when performing operations on a DataFrame or Series to decide whether the changes made should affect the original data or not. When you use inplace=True, any changes you make to the DataFrame or Series will modify the original data directly. It means that you’re altering the existing data, and you don’t need to assign the result to a new variable. When omitting inplace=True, Pandas by default creates a new copy of the DataFrame or Series with the changes applied. This means that the original data remains unchanged, and you need to assign the result to a new variable if you want to keep the modified data.\n\n\n\n# Replace empty strings with NaN values\ndf.replace(' ', np.nan, inplace=True)\ndf.head(5)\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n1\nADAX\nAda\nOK\n34.80\n-96.67\n2019\n4\n15\n15\n20\n...\n40\nNaN\nNaN\nS\n12\n20\n1011.13\n78\n48\nNaN\n\n\n2\nALTU\nAltus\nOK\n34.59\n-99.34\n2019\n4\n15\n15\n20\n...\n39\nNaN\n82\nSSW\n19\n26\n1007.86\n82\n45\nNaN\n\n\n3\nALV2\nAlva\nOK\n36.71\n-98.71\n2019\n4\n15\n15\n20\n...\n32\nNaN\n82\nS\n20\n26\n1004.65\n84\n40\nNaN\n\n\n4\nANT2\nAntlers\nOK\n34.25\n-95.67\n2019\n4\n15\n15\n20\n...\n35\nNaN\nNaN\nS\n11\n20\n1013.64\n78\n38\nNaN\n\n\n\n\n5 rows × 22 columns\n\n\n\n\n\n\n\n\n\nTip\n\n\n\nThe previous solution is not the best. We could have resolved the issue with the empty strings by simply adding the following option na_values=' ' to the pd.read_csv() function, like this: df = pd.read_csv(data_url, na_values=’ ’). This will automatically populate all cells that contain ' ' with NaN values.\n\n\n\nMatch specific stations\n\nidx_acme = df['STID'].str.match('ACME')\ndf[idx_acme]\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n\n\n1 rows × 22 columns\n\n\n\n\nidx_starts_with_A = df['STID'].str.match('A')\ndf[idx_starts_with_A]\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n1\nADAX\nAda\nOK\n34.80\n-96.67\n2019\n4\n15\n15\n20\n...\n40\nNaN\nNaN\nS\n12\n20\n1011.13\n78\n48\nNaN\n\n\n2\nALTU\nAltus\nOK\n34.59\n-99.34\n2019\n4\n15\n15\n20\n...\n39\nNaN\n82\nSSW\n19\n26\n1007.86\n82\n45\nNaN\n\n\n3\nALV2\nAlva\nOK\n36.71\n-98.71\n2019\n4\n15\n15\n20\n...\n32\nNaN\n82\nS\n20\n26\n1004.65\n84\n40\nNaN\n\n\n4\nANT2\nAntlers\nOK\n34.25\n-95.67\n2019\n4\n15\n15\n20\n...\n35\nNaN\nNaN\nS\n11\n20\n1013.64\n78\n38\nNaN\n\n\n5\nAPAC\nApache\nOK\n34.91\n-98.29\n2019\n4\n15\n15\n20\n...\n41\nNaN\nNaN\nS\n23\n29\n1008.9\n80\n49\nNaN\n\n\n6\nARD2\nArdmore\nOK\n34.19\n-97.09\n2019\n4\n15\n15\n20\n...\n41\nNaN\nNaN\nS\n18\n26\n1011.43\n77\n50\nNaN\n\n\n7\nARNE\nArnett\nOK\n36.07\n-99.90\n2019\n4\n15\n15\n20\n...\n10\nNaN\n85\nSW\n22\n32\n1005.13\nNaN\nNaN\nNaN\n\n\n\n\n8 rows × 22 columns\n\n\n\n\nidx_has_A = df['STID'].str.contains('A')\ndf[idx_has_A].head(15)\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n1\nADAX\nAda\nOK\n34.80\n-96.67\n2019\n4\n15\n15\n20\n...\n40\nNaN\nNaN\nS\n12\n20\n1011.13\n78\n48\nNaN\n\n\n2\nALTU\nAltus\nOK\n34.59\n-99.34\n2019\n4\n15\n15\n20\n...\n39\nNaN\n82\nSSW\n19\n26\n1007.86\n82\n45\nNaN\n\n\n3\nALV2\nAlva\nOK\n36.71\n-98.71\n2019\n4\n15\n15\n20\n...\n32\nNaN\n82\nS\n20\n26\n1004.65\n84\n40\nNaN\n\n\n4\nANT2\nAntlers\nOK\n34.25\n-95.67\n2019\n4\n15\n15\n20\n...\n35\nNaN\nNaN\nS\n11\n20\n1013.64\n78\n38\nNaN\n\n\n5\nAPAC\nApache\nOK\n34.91\n-98.29\n2019\n4\n15\n15\n20\n...\n41\nNaN\nNaN\nS\n23\n29\n1008.9\n80\n49\nNaN\n\n\n6\nARD2\nArdmore\nOK\n34.19\n-97.09\n2019\n4\n15\n15\n20\n...\n41\nNaN\nNaN\nS\n18\n26\n1011.43\n77\n50\nNaN\n\n\n7\nARNE\nArnett\nOK\n36.07\n-99.90\n2019\n4\n15\n15\n20\n...\n10\nNaN\n85\nSW\n22\n32\n1005.13\nNaN\nNaN\nNaN\n\n\n8\nBEAV\nBeaver\nOK\n36.80\n-100.53\n2019\n4\n15\n15\n20\n...\n9\nNaN\n84\nSW\n17\n26\n1003.9\n91\n34\nNaN\n\n\n11\nBLAC\nBlackwell\nOK\n36.75\n-97.25\n2019\n4\n15\n15\n20\n...\n38\nNaN\nNaN\nSSW\n15\n23\n1007.02\n80\n44\nNaN\n\n\n20\nBYAR\nByars\nOK\n34.85\n-97.00\n2019\n4\n15\n15\n20\n...\n43\nNaN\nNaN\nS\n22\n32\n1010.64\n77\n49\nNaN\n\n\n21\nCAMA\nCamargo\nOK\n36.03\n-99.35\n2019\n4\n15\n15\n20\n...\n32\nNaN\n82\nS\n23\n29\n1005.56\nNaN\nNaN\nNaN\n\n\n22\nCARL\nLake Carl Blackwell\nOK\n36.15\n-97.29\n2019\n4\n15\n15\n20\n...\n36\nNaN\n80\nS\n17\n25\n1007.56\n80\n50\nNaN\n\n\n24\nCHAN\nChandler\nOK\n35.65\n-96.80\n2019\n4\n15\n15\n20\n...\n37\nNaN\nNaN\nSSW\n16\n27\n1009.35\n80\n48\nNaN\n\n\n28\nCLAY\nClayton\nOK\n34.66\n-95.33\n2019\n4\n15\n15\n20\n...\n36\nNaN\nNaN\nS\n9\n24\n1012.9\n78\n40\nNaN\n\n\n\n\n15 rows × 22 columns\n\n\n\n\nidx = df['NAME'].str.contains('Blackwell') & df['NAME'].str.contains('Lake')\ndf[idx]\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n22\nCARL\nLake Carl Blackwell\nOK\n36.15\n-97.29\n2019\n4\n15\n15\n20\n...\n36\nNaN\n80\nS\n17\n25\n1007.56\n80\n50\nNaN\n\n\n\n\n1 rows × 22 columns\n\n\n\nThe following line won’t work because the string matching is case sensitive:\nidx = df['NAME'].str.contains('LAKE')\n\nidx = df['NAME'].str.contains('Blackwell') | df['NAME'].str.contains('Lake')\ndf[idx]\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n11\nBLAC\nBlackwell\nOK\n36.75\n-97.25\n2019\n4\n15\n15\n20\n...\n38\nNaN\nNaN\nSSW\n15\n23\n1007.02\n80\n44\nNaN\n\n\n22\nCARL\nLake Carl Blackwell\nOK\n36.15\n-97.29\n2019\n4\n15\n15\n20\n...\n36\nNaN\n80\nS\n17\n25\n1007.56\n80\n50\nNaN\n\n\n\n\n2 rows × 22 columns\n\n\n\n\nidx = df['STID'].isin(['ACME','ALTU'])\ndf[idx]\n\n\n\n\n\n\n\n\nSTID\nNAME\nST\nLAT\nLON\nYR\nMO\nDA\nHR\nMI\n...\nRELH\nCHIL\nHEAT\nWDIR\nWSPD\nWMAX\nPRES\nTMAX\nTMIN\nRAIN\n\n\n\n\n0\nACME\nAcme\nOK\n34.81\n-98.02\n2019\n4\n15\n15\n20\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n2\nALTU\nAltus\nOK\n34.59\n-99.34\n2019\n4\n15\n15\n20\n...\n39\nNaN\n82\nSSW\n19\n26\n1007.86\n82\n45\nNaN\n\n\n\n\n2 rows × 22 columns" + }, + { + "objectID": "basic_concepts/plotting.html#dataset", + "href": "basic_concepts/plotting.html#dataset", + "title": "30  Plotting", + "section": "Dataset", + "text": "Dataset\nTo keep this plotting notebook simple, we will start by reading some daily environmental data recorded in a tallgrass prairie in the Kings Creek watershed, which is located within the Konza Prairie Biological Station near Manhattan, KS. The dataset includes the following variables:\n\n\n\n\n\n\n\n\n\nVariable Name\nUnits\nDescription\nSensor\n\n\n\n\ndatetime\n-\nTimestamp of the data record\n\n\n\npressure\nkPa\nAtmospheric pressure\nAtmos 41\n\n\ntmin\n°C\nMinimum temperature\nAtmos 41\n\n\ntmax\n°C\nMaximum temperature\nAtmos 41\n\n\ntavg\n°C\nAverage temperature\nAtmos 41\n\n\nrmin\n%\nMinimum relative humidity\nAtmos 41\n\n\nrmax\n%\nMaximum relative humidity\nAtmos 41\n\n\nprcp\nmm\nPrecipitation amount\nAtmos 41\n\n\nsrad\nMJ/m²\nSolar radiation\nAtmos 41\n\n\nwspd\nm/s\nWind speed\nAtmos 41\n\n\nwdir\ndegrees\nWind direction\nAtmos 41\n\n\nvpd\nkPa\nVapor pressure deficit\nAtmos 41\n\n\nvwc_5cm\nm³/m³\nVolumetric water content at 5 cm depth\nTeros 12\n\n\nvwc_20cm\nm³/m³\nVolumetric water content at 20 cm depth\nTeros 12\n\n\nvwc_40cm\nm³/m³\nVolumetric water content at 40 cm depth\nTeros 12\n\n\nsoiltemp_5cm\n°C\nSoil temperature at 5 cm depth\nTeros 12\n\n\nsoiltemp_20cm\n°C\nSoil temperature at 20 cm depth\nTeros 12\n\n\nsoiltemp_40cm\n°C\nSoil temperature at 40 cm depth\nTeros 12\n\n\nbattv\nmillivolts\nBattery voltage of the datalogger\nAA Batt.\n\n\ndischarge\nm³/s\nStreamflow\nUSGS gauge\n\n\n\n\n# Import Numpy and Pandas modules\nimport numpy as np\nimport pandas as pd\n\n\n# Read some tabulated weather data\ndf = pd.read_csv('../datasets/kings_creek_2022_2023_daily.csv',\n parse_dates=['datetime'])\n\n# Display a few rows to inspect column headers and data\ndf.head(3)\n\n\n\n\n\n\n\n\ndatetime\npressure\ntmin\ntmax\ntavg\nrmin\nrmax\nprcp\nsrad\nwspd\nwdir\nvpd\nvwc_5cm\nvwc_20cm\nvwc_40cm\nsoiltemp_5cm\nsoiltemp_20cm\nsoiltemp_40cm\nbattv\ndischarge\n\n\n\n\n0\n2022-01-01\n96.838\n-14.8\n-4.4\n-9.6\n78.475\n98.012\n0.25\n2.098\n5.483\n0.969\n0.028\n0.257\n0.307\n0.359\n2.996\n5.392\n7.425\n8714.833\n0.0\n\n\n1\n2022-01-02\n97.995\n-20.4\n-7.2\n-13.8\n50.543\n84.936\n0.25\n9.756\n2.216\n2.023\n0.072\n0.256\n0.307\n0.358\n2.562\n4.250\n6.692\n8890.042\n0.0\n\n\n2\n2022-01-03\n97.844\n-9.4\n8.8\n-0.3\n40.622\n82.662\n0.50\n9.681\n2.749\n5.667\n0.262\n0.255\n0.307\n0.358\n2.454\n3.917\n6.208\n8924.833\n0.0" + }, + { + "objectID": "basic_concepts/plotting.html#matplotlib-module", + "href": "basic_concepts/plotting.html#matplotlib-module", + "title": "30  Plotting", + "section": "Matplotlib module", + "text": "Matplotlib module\nMatplotlib is a powerful and widely-used Python library for creating high-quality static and animated visualizations with a few lines of code that are suitable for scientific research. Matplotlib integrates well with other libraries like Numpy and Pandas, and can generate a wide range of graphs and has an extensive gallery of examples, so in this tutorial we will go over a few examples to learn the syntax, properties, and methods available to users in order to customize figures. To learn more visit Matplotlib’s official documentation.\n\nComponents of Matplotlib figures\n\n\n\nComponents of a Matplotlib figure. Source:matplotlib.org\n\n\n\nFigure: The entire window that everything is drawn on. The top-level container for all the elements.\nAxes: The part of the figure where the data is plotted, including any axes labeling, ticks, and tick labels. It’s the area that contains the plot elements.\nPlotting area: The space where the data points are visualized. It’s contained within the axes.\nAxis: These are the line-like objects and take care of setting the graph limits and generating the ticks and tick labels.\nTicks and Tick Labels: The marks on the axis to denote data points and the labels assigned to these ticks.\nLabels: Descriptive text added to the x axis and y axis to identify what each represents.\nTitle: A text label placed at the top of the axes to provide a summary or comment about the plot.\nLegend: A small area describing the different elements or data series of the plot. It’s used to identify plots represented by different colors, markers, or line styles.\n\nEssentially, a Matplotlib figure is an assembly of interconnected objects, each customizable through various properties and functions. When a figure is created, attributes such as the figure dimensions, axes properties, tick marks, font size of labels, and more come with pre-set default values. Understanding this object hierarchy is key customize figures to your visualization needs.\n\n\nMatplotlib syntax\nMatplotlib has two syntax styles or interfaces for creating figures:\n\nfunction-based interface (easy) that resembles Matlab’s plotting syntax. This interface relies on using the plt.___<function>____ construction for adding/modifying each component of a figure. It is simpler and more straightforward that the object-based interface (see below), so the function-based style is sometimes easier for beginners or students with background in Matlab. The main disadvantage of this method is that is implicit, meaning that the axes object (with all its attributes and methods) remains temporarily in the background since we are not saving it into a variable. This means that if we want to add/remove/modify something later on in one axes, we don’t have that object available to us to implement the changes. One option is to get the current axes using plt.gca(), but we need to do this before adding another axes (say another subplot) to the figure. If you don’t need to create sophisticated figures, then this method usually works just fine.\nobject-based interface (advanced) that offers more flexibility and control, particularly when dealing with multiple axes. In this interface, the figure and axes objects are explicit, meaning that each figure and axes object are stored as regular variables that provide the programmer access to all configuration options at any point in the script. The downside is that this syntax is a bit more verbose and sometimes less intuitive to beginners compared tot he function-based approach. The official documentation typically favors the object-based syntax, so it is good to become familair with it.\n\nBut don’t panic, the syntax between these two methods is not that different. Below I added more syntax details, a cheat sheet to help you understand some of the differences, and several examples using real data. In this article you can learn more about the pros and cons of each style.\n\nFunction-based syntax\n\n# Sample data\nx = [1, 2, 3, 4]\ny = [10, 20, 25, 30]\n\n# Create figure and plot\nplt.figure(figsize=(4,4))\nplt.plot(x, y)\nplt.title(\"Simple Line Plot\")\nplt.xlabel(\"X-axis\")\nplt.ylabel(\"Y-axis\")\nplt.show()\n\n\nObject-based syntax\n\n# Sample data\nx = [1, 2, 3, 4]\ny = [10, 20, 25, 30]\n\n# Create figure and axes\nfig, ax = plt.subplots(figsize=(4,4))\nax.plot(x, y)\nax.set_title(\"Simple Line Plot\")\nax.set_xlabel(\"X-axis\")\nax.set_ylabel(\"Y-axis\")\nplt.show()\n\n\nMatplotlib Cheat Sheet\n\n\n\n\n\n\n\n\nOperation\nfunction-based syntax\nobject-based syntax\n\n\n\n\nCreate figure\nplt.figure()\nfig,ax = plt.subplots() fig,ax = plt.subplots(1,1)\n\n\nSimple line or scatter plot\nplt.plot(x, y)plt.scatter(x, y)\nax.plot(x, y)ax.scatter(x, y)\n\n\nAdd axis labels\nplt.xlabel('label', fontsize=size)plt.ylabel('label', fontsize=size)\nax.set_xlabel('label', fontsize=size)ax.set_ylabel('label', fontsize=size)\n\n\nChange font size of tick marks\nplt.xticks(fontsize=size)plt.yticks(fontsize=size)\nax.tick_params(axis='both', labelsize=size)\n\n\nAdd a legend\nplt.legend()\nax.legend()\n\n\nRemove tick marks and labels\nplt.tick_params(axis='both', which='both', bottom=False, top=False, labelbottom=False)\nax.tick_params(axis='both', which='both', bottom=False, top=False, labelbottom=False)\n\n\nRemove tick labels only\nplt.gca().tick_params(axis='x', labelbottom=False)\nax.tick_params(axis='x', labelbottom=False)\n\n\nAdd a title\nplt.title('title')\nax.set_title('title')\n\n\nAdd a secondary axis\nplt.twinx()\nax_secondary = ax.twinx()\n\n\nRotate tick labels\nplt.xticks(rotation=angle)plt.yticks(rotation=angle)\nax.tick_params(axis='x', rotation=angle)ax.tick_params(axis='y', rotation=angle)\n\n\nChange scale\nplt.xscale('log')plt.yscale('log')\nax.set_xscale('log')ax.set_yscale('log')\n\n\nChange axis limits\nplt.xlim([xmin, xmax])plt.ylim([ymin, ymax])\nax.set_xlim([xmin, xmax])ax.set_ylim([ymin, ymax])\n\n\nCreate subplots\nplt.subplots(1, 2, 1)plt.subplots(2, 2, 1)\nfig, (ax1, ax2) = plt.subplots(1, 2)fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2)fig, axs = plt.subplots(2, 2); axs[0, 0].plot(x, y)\n\n\nChange xaxis dateformat\nfmt = mdates.DateFormatter('%b-%y') plt.gca().xaxis.set_major_formatter(fmt)\nfmt = mdates.DateFormatter('%b-%y') ax.xaxis.set_major_formatter(fmt)\n\n\n\n\n# Import matplotlib modules\nimport matplotlib.pyplot as plt\nimport matplotlib.dates as mdates\n\n\n\n\nAccess and modify plot configuration properties globally\nWe can use the code below to print the default value of all the properties within Matplotlib. You can also use this to set global properties.\n\n# Print all default properties (warning output is long!)\nplt.rcParams\n\n\n# Inspect some default properties\nprint(plt.rcParams['font.family'])\nprint(plt.rcParams['font.size'])\nprint(plt.rcParams['axes.labelsize'])\nprint(plt.rcParams['xtick.labelsize'])\nprint(plt.rcParams['ytick.labelsize'])\n\n# Remove comment to update the value of these properties\n# Changes will affect all charts in this notebook\n# plt.rcParams.update({'font.family':'Verdana'})\n# plt.rcParams.update({'font.size':11})\n# plt.rcParams.update({'axes.labelsize':14})\n# plt.rcParams.update({'xtick.labelsize':12})\n# plt.rcParams.update({'ytick.labelsize':12})\n\n\n# Reset to default configuration values (this will undo the previous line)\n# plt.rcdefaults()\n\n['sans-serif']\n10.0\nmedium\nmedium\nmedium\n\n\n\n\nLine plot\nA common plot when working with meteorological data is to show maximum and minimum air temperature.\n\n# Create figure\nplt.figure(figsize=(8,4)) # If you set dpi=300 the figure is much better quality\n\n# Add lines to axes\nplt.plot(df['datetime'], df['tmax'], color='tomato', linewidth=1, label='Tmax')\nplt.plot(df['datetime'], df['tmin'], color='navy', linewidth=1, label='Tmin')\n\n# Customize chart attributes\nplt.title('Kings Creek Watershed', \n fontdict={'family':'Verdana', 'color':'black', 'weight':'bold', 'size':14})\nplt.xlabel('Time', fontsize=10)\nplt.ylabel('Air Temperature (Celsius)', fontsize=10)\nplt.xticks(fontsize=10, rotation=20)\nplt.yticks(fontsize=10)\nplt.legend(fontsize=10)\n\n# Create custom dateformat for x-axis\ndate_format = mdates.DateFormatter('%b-%y')\n\n# We don't have the axes object saved into a variable, so to set the date format \n# we need to get the current axes (gca). If we were adding more axes to this figure,\n# then gca() will return the current axes\nplt.gca().xaxis.set_major_formatter(date_format)\n\n# Save figure. Call before plt.show()\n#plt.savefig('line_plot.jpg', dpi=300, facecolor='w', pad_inches=0.1)\n\n# Render figure\nplt.show()\n\n\n\n\n\nObject-based code\nfig, ax = plt.subplots(1,1,figsize=(8,4), dpi=300)\nax.plot(df['datetime'], df['tmax'], label='Tmax')\nax.plot(df['datetime'], df['tmin'], label='Tmin')\nax.set_xlabel('Time', fontsize=10)\nax.set_ylabel('Air Temperature (Celsius)', fontsize=10)\nax.tick_params(axis='both', labelsize=10)\nax.legend(fontsize=10)\ndate_format = mdates.DateFormatter('%b-%y')\n\n# Here we have the axes object saved into the `ax` variable, so it is explicit and \n# we just need to access the method within the object. \n# We could do this step later, even if we create other axes with different variable names.\nax.xaxis.set_major_formatter(date_format) \n\n# Save figure. Call before plt.show()\nplt.savefig('line_plot.jpg', dpi=300, facecolor='w', pad_inches=0.1)\n\n# Render figure\nplt.show()\n\n\n\nScatter plot\nLet’s inspect soil temperature data a 5 and 40 cm depth and see how similar or different these two variables are. A 1:1 to line will serve as the reference of perfect equality.\n\n# Scatter plot\nplt.figure(figsize=(5,4))\nplt.scatter(df['soiltemp_5cm'], df['soiltemp_40cm'],\n marker='o', facecolor=(0.8, 0.1, 0.1, 0.3), \n edgecolor='k', linewidth=0.5, label='Observations')\nplt.plot([-5, 35], [-5, 35], linestyle='--', color='k', label='1:1 line') # 1:1 line\nplt.title('Scatter plot', fontsize=12, fontweight='normal')\nplt.xlabel('Soil temperature 5 cm $\\mathrm{\\degree{C}}$', size=12)\nplt.ylabel('Soil temperature 40 cm $\\mathrm{\\degree{C}}$', size=12)\nplt.xticks(fontsize=10)\nplt.yticks(fontsize=10)\nplt.xlim([-5, 35])\nplt.ylim([-5, 35])\nplt.legend(fontsize=10)\nplt.grid()\n\n# Use the following lines to hide the axis tick marks and labels\n#plt.xticks([])\n#plt.yticks([])\n\nplt.show()\n \n\n\n\n\n\n\n\n\n\n\nLaTeX italics\n\n\n\nTo remove the italics style in your units and equations use \\mathrm{ } within your LaTeX text.\n\n\n\nObject-based syntax\n# Scatter plot\nfig, ax = plt.subplots(1, 1, figsize=(6,5), edgecolor='k')\nax.scatter(df['soiltemp_5cm'], df['soiltemp_40cm'], \n marker='o', facecolor=(0.8, 0.1, 0.1, 0.3),\n edgecolor='k', linewidth=0.5, label='Observations')\nax.plot([-5, 35], [-5, 35], linestyle='--', color='k', label='1:1 line')\nax.set_title('Scatter plot', fontsize=12, fontweight='normal')\nax.set_xlabel(\"Soil temperature 5 cm $^\\degree{C}$\", size=12)\nax.set_ylabel(\"Soil temperature 40 cm $^\\degree{C}$\", size=12)\nax.set_xlim([-5, 35])\nax.set_ylim([-5, 35])\nax.tick_params(axis='both', labelsize=12)\nax.grid(True)\nplt.show()\n\n\n\nHistogram\nOne of the most common and useful charts to describe the distribution of a dataset is the histogram.\n\n# Histogram\nplt.figure(figsize=(6,5))\nplt.hist(df['vwc_5cm'], bins='scott', density=False, \n facecolor='g', alpha=0.75, edgecolor='black', linewidth=1.2)\nplt.title('Soil moisture distribution', fontsize=12)\nplt.xlabel('Soil moisture $cm^3 cm^{-3}$', fontsize=12)\nplt.ylabel('Count', fontsize=12)\nplt.xticks(fontsize=10)\nplt.yticks(fontsize=10)\n\navg = df['vwc_5cm'].mean()\nann_val = f\"Mean = {avg:.3f} \" \nann_units = \"$\\mathrm{cm^3 cm^{-3}}$\"\n\nplt.text(avg-0.01, 85, ann_val + ann_units, \n size=10, rotation=90, family='color='black')\nplt.axvline(df['vwc_5cm'].mean(), linestyle='--', color='k')\nplt.show()\n\n\n\n\n\nObject-based syntax\n# Histogram\nfig, ax = plt.subplots(figsize=(6,5))\nax.hist(df['vwc_5cm'], bins='scott', density=False, \n facecolor='g', alpha=0.75, edgecolor='black', linewidth=1.2)\n\navg = df['vwc_5cm'].mean()\nann_val = f\"Mean = {avg:.3f} \" \nann_units = \"$\\mathrm{cm^3 cm^{-3}}$\"\n\nax.text(avg-0.01, 85, ann_val + ann_units, size=10, rotation=90, color='black')\nax.set_xlabel('Volumetric water content $cm^3 cm^{-3}$', fontsize=12)\nax.set_ylabel('Count', fontsize=12)\nax.tick_params('both', labelsize=12)\nax.set_title('Soil moisture distribution', fontsize=12)\nax.axvline(df['vwc_5cm'].mean(), linestyle='--', color='k')\nplt.show()\n\n\n\nSubplots\nIn fields like agronomy, environmental science, hydrology, and meteorology sometimes we want to show multiple variables in one figure, but in different charts. Other times we want to show the same variable, but in separate charts for different locations or sites. In Matplotlib we can achieve this using subplots.\n\n# Subplots with all labels and axis tick marks\n\n# Define date format\ndate_format = mdates.ConciseDateFormatter(mdates.AutoDateLocator)\n\n# Create figure\nplt.figure(figsize=(10,6))\n\n# Set width and height spacing between subplots\nplt.subplots_adjust(wspace=0.3, hspace=0.4)\n\n# Add superior title for entire figure\nplt.suptitle('Kings Creek temperatures 2022-2023')\n\n# Subplot 1 of 4\nplt.subplot(2, 2, 1)\nplt.plot(df[\"datetime\"], df[\"tavg\"])\nplt.title('Air temperature', fontsize=12)\nplt.ylabel('Temperature', fontsize=12)\nplt.ylim([-20, 40])\nplt.gca().xaxis.set_major_formatter(date_format)\nplt.text( df['datetime'].iloc[0], 32, 'A', fontsize=14)\n\n# Hide tick labels on the x-axis. Add bottom=False to remove the ticks\nplt.gca().tick_params(axis='x', labelbottom=False) \n\n# Subplot 2 of 4\nplt.subplot(2, 2, 2)\nplt.plot(df[\"datetime\"], df[\"soiltemp_5cm\"])\nplt.title('Soil temperature 5 cm', size=12)\nplt.ylabel('Temperature', size=12)\nplt.ylim([-20, 40])\nplt.gca().xaxis.set_major_formatter(date_format)\nplt.text( df['datetime'].iloc[0], 32, 'B', fontsize=14)\nplt.gca().tick_params(axis='x', labelbottom=False)\n\n# Subplot 3 of 4\nplt.subplot(2, 2, 3)\nplt.plot(df[\"datetime\"], df[\"soiltemp_20cm\"])\nplt.title('Soil temperature 20 cm', size=12)\nplt.ylabel('Temperature', size=12)\nplt.ylim([-20, 40])\nplt.gca().xaxis.set_major_formatter(date_format)\nplt.text( df['datetime'].iloc[0], 32, 'C', fontsize=14)\n\n\n# Subplot 4 of 4\nplt.subplot(2, 2, 4)\nplt.text( df['datetime'].iloc[0], 32, 'D', fontsize=14)\nplt.plot(df[\"datetime\"], df[\"soiltemp_40cm\"])\nplt.title('Soil temperature 40 cm', size=12)\nplt.ylabel('Temperature', size=12)\nplt.ylim([-20, 40])\nplt.gca().xaxis.set_major_formatter(date_format)\nplt.text( df['datetime'].iloc[0], 32, 'D', fontsize=14)\n\n# Adjust height padding (hspace) and width padding (wspace)\n# between subplots using fractions of the average axes height and width\nplt.subplots_adjust(hspace=0.3, wspace=0.3)\n\n# Render figure\nplt.show()\n\n\n\n\n\nObject-based syntax\n# Subplots\n\n# Define date format\ndate_format = mdates.ConciseDateFormatter(mdates.AutoDateLocator)\n\n# Create figure (each row is returned as a tuple of axes)\nfig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(2, 2, figsize=(10,6))\n\n# Set width and height spacing between subplots\nfig.subplots_adjust(wspace=0.3, hspace=0.4)\n\n# Add superior title for entire figure\nfig.suptitle('Kings Creek temperatures 2022-2023')\n\nax1.set_title('Air temperature', size=12)\nax1.set_ylabel('Temperature', size=12)\nax1.set_ylim([-20, 40])\nax1.xaxis.set_major_formatter(date_format)\nax1.text( df['datetime'].iloc[0], 32, 'A', fontsize=14)\nax1.tick_params(axis='x', labelbottom=False)\n\nax2.set_title('Soil temperature 5 cm', size=12)\nax2.set_ylabel('Temperature', size=12)\nax2.set_ylim([-20, 40])\nax2.xaxis.set_major_formatter(date_format)\nax2.text( df['datetime'].iloc[0], 32, 'B', fontsize=14)\nax2.tick_params(axis='x', labelbottom=False)\n\nax3.set_title('Soil temperature 20 cm', size=12)\nax3.set_ylabel('Temperature', size=12)\nax3.set_ylim([-20, 40])\nax3.xaxis.set_major_formatter(date_format)\nax3.text( df['datetime'].iloc[0], 32, 'C', fontsize=14)\n\nax4.set_title('Soil temperature 40 cm', size=12)\nax4.set_ylabel('Temperature', size=12)\nax4.set_ylim([-20, 40])\nax4.xaxis.set_major_formatter(date_format)\nax4.text( df['datetime'].iloc[0], 32, 'D', fontsize=14)\n\n# ------ ADDING ALL LINES AT THE END -----\n# To illustrate the power of the object-based notation I set\n# all the line plots here at the end. In the function-based syntax you are forced\n# to set all the elements and attributes within the block of code for that subplot\nax1.plot(df[\"datetime\"], df[\"tavg\"])\nax2.plot(df[\"datetime\"], df[\"soiltemp_5cm\"])\nax3.plot(df[\"datetime\"], df[\"soiltemp_20cm\"])\nax4.plot(df[\"datetime\"], df[\"soiltemp_40cm\"])\n\nplt.show()\n\n\n\nFill area plots\nTo illustrate the use of filled area charts we will use a time series of drought conditions obtained from the U.S. Drought Monitor.\n\n# Read U.S. Drought Monitor data\ndf_usdm = pd.read_csv('../datasets/riley_usdm_20210701_20220916.csv',\n parse_dates=['MapDate'], date_format='%Y%m%d')\ndf_usdm.head(3)\n\n\n\n\n\n\n\n\nMapDate\nFIPS\nCounty\nState\nNone\nD0\nD1\nD2\nD3\nD4\nValidStart\nValidEnd\nStatisticFormatID\n\n\n\n\n0\n2022-09-13\n20161\nRiley County\nKS\n0.00\n100.00\n0.0\n0.0\n0.0\n0.0\n2022-09-13\n2022-09-19\n1\n\n\n1\n2022-09-06\n20161\nRiley County\nKS\n0.00\n100.00\n0.0\n0.0\n0.0\n0.0\n2022-09-06\n2022-09-12\n1\n\n\n2\n2022-08-30\n20161\nRiley County\nKS\n81.02\n18.98\n0.0\n0.0\n0.0\n0.0\n2022-08-30\n2022-09-05\n1\n\n\n\n\n\n\n\n\n# Fill area plot\nfig = plt.figure(figsize=(8,3))\nplt.title('Drough COnditions for Riley County, KS')\nplt.fill_between(df_usdm['MapDate'], df_usdm['D0'], \n color='yellow', edgecolor='k', label='D0-D4')\nplt.fill_between(df_usdm['MapDate'], df_usdm['D1'], \n color='navajowhite', edgecolor='k', label='D1-D4')\nplt.fill_between(df_usdm['MapDate'], df_usdm['D2'], \n color='orange', edgecolor='k', label='D2-D4')\nplt.fill_between(df_usdm['MapDate'], df_usdm['D3'], \n color='red', edgecolor='k', label='D3-D4')\nplt.fill_between(df_usdm['MapDate'], df_usdm['D4'], \n color='maroon', edgecolor='k', label='D4')\n\nplt.ylim(0,105)\nplt.legend(bbox_to_anchor=(1.18, 1.05))\nplt.ylabel('Area (%)', fontsize=12)\nplt.show()\n\n\n\n\n\nObject-based syntax\n# Fill area plot\n\nfig, ax = plt.subplots(figsize=(8,3))\nax.set_title('Drough Conditions for Riley County, KS')\nax.fill_between(df_usdm['MapDate'], df_usdm['D0'], \n color='yellow', edgecolor='k', label='D0-D4')\nax.fill_between(df_usdm['MapDate'], df_usdm['D1'], \n color='navajowhite', edgecolor='k', label='D1-D4')\nax.fill_between(df_usdm['MapDate'], df_usdm['D2'], \n color='orange', edgecolor='k', label='D2-D4')\nax.fill_between(df_usdm['MapDate'], df_usdm['D3'], \n color='red', edgecolor='k', label='D3-D4')\nax.fill_between(df_usdm['MapDate'], df_usdm['D4'], \n color='maroon', edgecolor='k', label='D4')\n\nax.set_ylim(0,105)\nax.legend(bbox_to_anchor=(1.18, 1.05))\nax.set_ylabel('Area (%)', fontsize=12)\nplt.show()\n\n\n\nSecondary Y axis plots\nSometimes we want to show two related variables with different range or entirely different units in the same chart. In this case, two chart axes can share the same x-axis but have two different y-axes. A typical example of this consists of displaying soil moisture variations together with precipitation. While less common, it is also possible for two charts to share the same y-axis and have two different x-axes.\n\n# Creating plot with secondary y-axis\nplt.figure(figsize=(8,4))\n\nplt.plot(df[\"datetime\"], df[\"vwc_5cm\"], '-k')\nplt.xlabel('Time', size=12)\nplt.ylabel('Volumetric water content', color='k', size=12)\n\nplt.twinx()\n\nplt.bar(df[\"datetime\"], df[\"prcp\"], width=2, color='tomato', linestyle='-')\nplt.ylabel('Precipitation (mm)', color='tomato', size=12)\nplt.ylim([0, 50])\n\nplt.show()\n\n\n\n\n\nObject-based syntax\n# Creating plot with secondary y-axis\nfig, ax = plt.subplots(figsize=(8,4), facecolor='w')\n\nax.plot(df[\"datetime\"], df[\"vwc_5cm\"], '-k')\nax.set_xlabel('Time', size=12)\nax.set_ylabel('Volumetric water content', color='k', size=12)\n\nax2 = ax.twinx()\n\nax2.bar(df[\"datetime\"], df[\"prcp\"], width=2, color='tomato', linestyle='-')\nax2.set_ylabel('Precipitation (mm)', color='tomato', size=12)\nax2.set_ylim([0, 50])\n\nplt.show()\n\n\n\nDonut charts\nIn agronomy, often times we need to represent complex cropping systems with multiple crops and fallow periods. In a single figure, donut charts can display:\n\nthe crop sequence,\nthe duration of each crop in the sequence,\nthe time of the year in which each crop is in the field from planting to harvesting,\nand the number of yers of the rotation\n\nHere is an example for a typical two-year crop rotation in the U.S. Midwest that consists of winter wheat, double crop soybeans, a winter fallow period, and corn.\n\n# Crops of the rotation\ncrops = ['W','SB','F','C'] # Crops labels\ncrop_values = [9, 5, 4, 6] # Duration of each crop in the rotation\ncrop_colors = ['wheat','forestgreen','whitesmoke','orange']\n \n# Months of the year\nmonths = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'] * 2\nmonth_values = [1] * len(months) # Each month is given a value of 1 for equal distribution\n\n# Here are other alternatives for coloring the month tiles\n#month_colors = plt.cm.tab20c(np.linspace(0, 1, 12))\n#month_colors = ['tomato']*12 + ['lightgreen']*12\n\n# Years of the rotation\nyears = ['Y1','Y2']\nyear_values = [12] * 2\nyear_colors = ['hotpink','lightblue']\n\n\n# Create figure and axis\nplt.figure(figsize=(5,5))\nplt.title('Wheat-Soybean-Corn Rotation')\n\n# Crops donut chart\nplt.pie(crop_values, radius=1.65, labels=crops, \n wedgeprops=dict(width=0.35, edgecolor='k', linewidth=0.5),\n startangle=90, labeldistance=0.83, rotatelabels=True,\n counterclock=False, colors=crop_colors)\n\n# Month donut chart\nplt.pie(month_values, radius=1.2, labels=months, \n wedgeprops=dict(width=0.3, edgecolor='k', linewidth=0.5),\n startangle=45, labeldistance=0.8, rotatelabels=True,\n counterclock=False, colors='w')\n\n# Years donut chart\nplt.pie(year_values, radius=0.8, labels=years, \n wedgeprops=dict(width=0.25, edgecolor='k', linewidth=0.5), \n startangle=45, labeldistance=0.65, rotatelabels=True, counterclock=False,\n colors=year_colors)\n\n# Add annotation (83% of the rotation time is with crops)\nplt.text(-0.25, -0.05, f\"{83:.0f}%\", fontsize=20)\n\n# Equal aspect ratio ensures that pie charts are drawn as circles\nplt.axis('equal')\nplt.tight_layout()\n\nplt.show()\n\n\n\n\n\n\nThemes\nIn addition to the default style, Matplotlib also offers a variery of pre-defined themes. To see some examples visit the following websites:\nGallery 1 at: https://matplotlib.org/gallery/style_sheets/style_sheets_reference.html\nGallery 2 at: https://tonysyu.github.io/raw_content/matplotlib-style-gallery/gallery.html\n\n# Run this line to see all the styling themes available\nprint(plt.style.available)\n\n['Solarize_Light2', '_classic_test_patch', '_mpl-gallery', '_mpl-gallery-nogrid', 'bmh', 'classic', 'dark_background', 'fast', 'fivethirtyeight', 'ggplot', 'grayscale', 'seaborn-v0_8', 'seaborn-v0_8-bright', 'seaborn-v0_8-colorblind', 'seaborn-v0_8-dark', 'seaborn-v0_8-dark-palette', 'seaborn-v0_8-darkgrid', 'seaborn-v0_8-deep', 'seaborn-v0_8-muted', 'seaborn-v0_8-notebook', 'seaborn-v0_8-paper', 'seaborn-v0_8-pastel', 'seaborn-v0_8-poster', 'seaborn-v0_8-talk', 'seaborn-v0_8-ticks', 'seaborn-v0_8-white', 'seaborn-v0_8-whitegrid', 'tableau-colorblind10']\n\n\n\n# Change plot defaults to ggplot style (similar to ggplot R language library)\n# Use plt.style.use('default') to revert style\n\nplt.style.use('ggplot') # Use plt.style.use('default') to revert.\n\nplt.figure(figsize=(8,4))\nplt.plot(df[\"datetime\"], df[\"srad\"], '-g')\nplt.ylabel(\"Solar radiation $MJ \\ m^{-2} \\ day^{-1}$\")\n\nplt.show()" + }, + { + "objectID": "basic_concepts/plotting.html#bokeh-module", + "href": "basic_concepts/plotting.html#bokeh-module", + "title": "30  Plotting", + "section": "Bokeh module", + "text": "Bokeh module\nThe Bokeh plotting library was designed for creating interactive visualizations for modern web browsers. Compared to Matplotlib, which excels in creating static plots, Bokeh emphasizes interactivity, offering tools to create dynamic and interactive graphics. Additionally, Bokeh integrates well with the Pandas library and provides a consistent and standardized syntax. This focus on interactivity and ease of use makes Bokeh well suited for web-based data visualizations and applications.\nUnlike Matplotlib, where you typically import the entire library with a single command, Bokeh is organized into various sub-modules catered to different functionalities. This structure means that you import specific components from their respective modules, which aligns with the functionality you intend to use. While this might require a bit more upfront learning about the library, it also means that you are only importing what you need.\n\n# Import Bokeh modules\nfrom bokeh.plotting import figure, output_notebook, show\nfrom bokeh.models import HoverTool\n\n# Initialize the bokeh for the notebook\n# If everything went correct you should see the Bokeh icon displaying below.\noutput_notebook()\n\n\n \n Loading BokehJS ..." + }, + { + "objectID": "basic_concepts/plotting.html#interactive-line-plot", + "href": "basic_concepts/plotting.html#interactive-line-plot", + "title": "30  Plotting", + "section": "Interactive line plot", + "text": "Interactive line plot\n\n# Create figure\np = figure(width=700, height=350, title='Kings Creek', x_axis_type='datetime')\n\n# Add line to the figure\np.line(df['datetime'], df['tmin'], line_color='blue', line_width=2, legend_label='Tmin')\n\n# Add another line. In this case I used a different, but equivalent, syntax\n# This syntax leverages the dataframe and its column names\np.line(source=df, x='datetime', y='tmax', line_color='tomato', line_width=2, legend_label='Tmax')\n\n# Customize figure properties\np.xaxis.axis_label = 'Time'\np.yaxis.axis_label = 'Air Temperature (Celsius)'\n\n# Set the font size of the x-axis and y-axis labels\np.yaxis.axis_label_text_font_size = '12pt' # Defined as a string using points simialr to Word\np.xaxis.axis_label_text_font_size = '12pt'\n\n# Set up the size of the labels in the major ticks\np.xaxis.major_label_text_font_size = '12pt'\np.yaxis.major_label_text_font_size = '12pt'\n\n# Add legend\np.legend.location = \"top_left\"\np.legend.title = \"Legend\"\np.legend.label_text_font_style = \"italic\"\np.legend.label_text_color = \"black\"\np.legend.border_line_width = 1\np.legend.border_line_color = \"navy\"\np.legend.border_line_alpha = 0.8\np.legend.background_fill_color = \"white\"\np.legend.background_fill_alpha = 0.9\n\n# Hover tool for interactive tooltips on mouse hover over the plot\np.add_tools(HoverTool(tooltips=[(\"Date:\", \"$x{%F}\"),(\"Temperature:\",\"$y{%0.1f} Celsius\")],\n formatters={'$x':'datetime', '$y':'printf'},\n mode='mouse'))\n# Display figure\nshow(p)" + }, + { + "objectID": "basic_concepts/plotting.html#seaborn-module", + "href": "basic_concepts/plotting.html#seaborn-module", + "title": "30  Plotting", + "section": "Seaborn module", + "text": "Seaborn module\nSeaborn is a plotting library based on Matplotlib, specifically tailored for statistical data visualization. It stands out in scientific research for its ability to create informative and attractive statistical graphics with ease. Seaborn integrates well with Pandas DataFrames and its default styles and color palettes are designed to be aesthetically pleasing and ready for publication. Seaborn offers complex visualizations like heatmaps, violin plots, boxplots, and matrix scatter plots.\n\n# Import Seaborn module\nimport seaborn as sns\n\n# Use the following style for a matplotlib-like style\n# Comment line to have a ggplot-like style\nsns.set_theme(style=\"ticks\")\n\n\nLine plot\n\n# Basic line plot\ndf_subset = df[['datetime', 'vwc_5cm', 'vwc_20cm', 'vwc_40cm']].copy()\ndf_subset['year'] = df_subset['datetime'].dt.year\n\nplt.figure(figsize=(8,4))\nsns.lineplot(data=df_subset, x='datetime', y='vwc_5cm', \n hue='year', palette=['tomato','navy']) # Can also use palette='Set1'\nplt.ylabel('Volumetric water content')\nplt.show()\n\n\n\n\n\n\nCorrelation matrix\n\n# Compute the correlation matrix\ncorr = df.corr(numeric_only=True)\n\n# Generate a mask for the upper triangle\nmask = np.triu(np.ones_like(corr, dtype=bool))\n\n# Draw the heatmap with the mask and correct aspect ratio\nsns.heatmap(corr, mask=mask, cmap='Spectral', vmax=.3, center=0,\n square=True, linewidths=.5, cbar_kws={\"shrink\": .5})\n\n<Axes: >\n\n\n\n\n\n\n\nScatterplot matrix\n\n# Create subset of main dataframe\ndf_subset = df[['datetime','tavg', 'vpd', 'srad', 'wspd']].copy()\ndf_subset['year'] = df_subset['datetime'].dt.year\n\nplt.figure(figsize=(8,6))\nsns.pairplot(data=df_subset, hue=\"year\", palette=\"Set2\")\nplt.show()\n\n<Figure size 800x600 with 0 Axes>\n\n\n\n\n\n\n\nHeatmap\nVisualization of air and soil temperature at 5, 20, and 40 cm depths on a weekly basis.\n\n# Summarize data by month\ndf_subset = df[['datetime', 'tavg', 'soiltemp_5cm', 'soiltemp_20cm', 'soiltemp_40cm']].copy()\ndf_subset['week'] = df['datetime'].dt.isocalendar().week\n\n# Average the values for both years on a weekly basis\ndf_subset = df_subset.groupby([\"week\"]).mean(numeric_only=True).round(2)\n \n# Create Heatmap\nplt.figure(figsize=(15,3))\nsns.heatmap(df_subset.T, annot=False, linewidths=1, cmap=\"RdBu_r\")\nplt.show()\n \n\n\n\n\n\n\nBoxplot\n\n# Summarize data by month\ndf_subset = df[['datetime', 'vwc_5cm']].copy()\ndf_subset['month'] = df['datetime'].dt.month\n\n# Draw a nested boxplot to show bills by day and time\nplt.figure(figsize=(8,6))\nsns.boxplot(data=df_subset, x=\"month\", y=\"vwc_5cm\", hue='month')\nsns.despine(offset=10, trim=True)\nplt.show()" + }, + { + "objectID": "basic_concepts/widgets.html#example-1-convert-bushels-to-metric-tons", + "href": "basic_concepts/widgets.html#example-1-convert-bushels-to-metric-tons", + "title": "31  Widgets", + "section": "Example 1: Convert bushels to metric tons", + "text": "Example 1: Convert bushels to metric tons\nA common task in agronomy is to convert grain yields from bushels to metric tons. We will use ipywidgets to create interactive dropdowns and slider widgets to facilitate the convertion between these two units for common crops.\n\n# Import modules\nimport ipywidgets as widgets\n\n\n# Define widget\ncrop_dropdown = widgets.Dropdown(options=['Barley','Corn','Sorghum','Soybeans','Wheat'],\n value='Wheat', description='Crop')\nbushels_slider = widgets.FloatSlider(value=40, min=0, max=200, step=1,\n description='Bushels/acre')\n\n# Define function\ndef bushels_to_tons(crop, bu):\n \"\"\"Function that converts bushels to metric tons for common crops.\n Source: https://grains.org/\n \"\"\"\n # Define constants\n lbs_per_ton = 0.453592/1000\n acres_per_ha = 2.47105\n \n # Convert bu -> lbs -> tons\n if crop == 'Barley':\n tons = bu * 48 * lbs_per_ton \n \n elif crop == 'Corn' or crop == 'Sorghum':\n tons = bu * 56 * lbs_per_ton\n \n elif crop == 'Wheat' or crop == 'Soybeans':\n tons = bu * 60 * lbs_per_ton\n \n # Convert acre -> hectares\n tons = round(tons * acres_per_ha, 2)\n return widgets.FloatText(value=tons, description='Tons/ha', disabled=True)\n\n\n# Define interactivity\nwidgets.interact(bushels_to_tons, crop=crop_dropdown, bu=bushels_slider);" + }, + { + "objectID": "basic_concepts/widgets.html#example-2-runoff-precipitation", + "href": "basic_concepts/widgets.html#example-2-runoff-precipitation", + "title": "31  Widgets", + "section": "Example 2: Runoff-Precipitation", + "text": "Example 2: Runoff-Precipitation\nWidgets are also a great tool to explore and learn how models representing real-world processes respond to input changes. In this example we will explore how the curve number of a soil is related to the amount of runoff for a range of precipitation amounts.\n\nimport ipywidgets as widgets\nimport matplotlib.pyplot as plt\nimport numpy as np\nplt.style.use('ggplot')\n\n\n# Create widget\ncn_slider = widgets.IntSlider(50, min=1, max=100, description='Curve number')\n\n# Create function\ndef estimate_runoff(cn):\n \"\"\"Function that computes runoff based on the \n curve number method proposed by the Soil Conservation Service\n Source: https://www.wikiwand.com/en/Runoff_curve_number\n\n Inputs\n cn : Curve number. 0 means fully permeable and 100 means fully impervious.\n \n Returns\n Figure of runoff as a function of precipitation\n \"\"\"\n\n P = np.arange(0, 12, step=0.01) # Precipitation in inches\n RO = np.zeros_like(P)\n S = 1000/cn - 10\n Ia = S * 0.05 # Initial abstraction (inches)\n idx = P > Ia\n RO[idx] = (P[idx] - Ia)**2 / (P[idx] - Ia + S)\n\n # Create figure\n plt.figure(figsize=(6,4))\n plt.plot(P,RO,'--k')\n plt.title('Curve Number Method')\n plt.xlabel('Precipitation (inches)')\n plt.ylabel('Runoff (inches)')\n plt.xlim([0,12])\n plt.ylim([0,12])\n return plt.show()\n\n# Define interactivity\nwidgets.interact(estimate_runoff, cn=cn_slider);" + }, + { + "objectID": "basic_concepts/sql_database.html#additional-software", + "href": "basic_concepts/sql_database.html#additional-software", + "title": "32  SQLite Database", + "section": "Additional software", + "text": "Additional software\nTo access and inspect the database I recommend using an open source tool like sqlitebrowser. With this tool you can also create, design, and edit sqlite databases, but we will do some of these steps using Python." + }, + { + "objectID": "basic_concepts/sql_database.html#key-commands", + "href": "basic_concepts/sql_database.html#key-commands", + "title": "32  SQLite Database", + "section": "Key commands", + "text": "Key commands\n\nCREATE TABLE: A SQL command used to create a new table in a database.\nINSERT: A SQL command used to add new rows of data to a table in the database.\nSELECT: A SQL command used to query data from a table, returning rows that match the specified criteria.\nUPDATE: A SQL command used to modify existing data in a table.\nDELETE: A SQL command used to remove rows from a table in the database." + }, + { + "objectID": "basic_concepts/sql_database.html#data-types", + "href": "basic_concepts/sql_database.html#data-types", + "title": "32  SQLite Database", + "section": "Data types", + "text": "Data types\nSQLite supports a variety of data types:\n\nTEXT: For storing character data. SQLite supports UTF-8, UTF-16BE, and UTF-16LE encodings.\nINTEGER: For storing integer values. The size can be from 1 byte to 8 bytes, depending on the magnitude of the value.\nREAL: For storing floating-point values. It is a double-precision (8-byte) floating point number.\nBLOB: Stands for Binary Large Object. Used to store data exactly as it was input, such as images, files, or binary data.\nNUMERIC: This type can be used for both integers and floating-point numbers. SQLite decides whether to use integer or real based on the value’s nature.\nBOOLEAN: SQLite does not have a separate boolean storage class. Instead, boolean values are stored as integers 0 (false) and 1 (true).\nDATE and TIME: SQLite does not have a storage class set aside for storing dates and/or times. Instead, they are stored as TEXT (as ISO8601 strings), REAL (as Julian day numbers), or INTEGER (as Unix Time, the number of seconds since 1970-01-01 00:00:00 UTC).\n\nIn SQLite the datatype you specify for a column acts more like a hint than a strict enforcement, allowing for flexibility in the types of data that can be inserted into a column. This is a distinctive feature compared to more rigid type systems in other database management systems." + }, + { + "objectID": "basic_concepts/sql_database.html#set-up-a-simple-database", + "href": "basic_concepts/sql_database.html#set-up-a-simple-database", + "title": "32  SQLite Database", + "section": "Set up a simple database", + "text": "Set up a simple database\nAfter importing the sqlite3 module we create a connection to a new SQLite database. If the file doesn’t exist, the SQLite module will create it. This is convenient since we don’t have to be constantly checking whether the database exists or worry about overwriting the database.\n\n# Import modules\nimport sqlite3\n\n\n# Connect to the database\nconn = sqlite3.connect('soils.db')\n\n\n# Create a cursor object using the cursor() method\ncursor = conn.cursor()\n\n# Create table\ncursor.execute('''CREATE TABLE soils\n (id INTEGER PRIMARY KEY, date TEXT, lat REAL, lon REAL, vwc INTEGER);''')\n\n# Save (commit) the changes\nconn.commit()\n\n\nAdd Data\nIn SQLite databases, the construction (?,?,?,?) is used as a placeholder for parameter substitution in SQL statements, especially with the INSERT, UPDATE, and SELECT commands. This construction offers the following advantages:\n\nSQL Injection Prevention: By using placeholders, you prevent SQL injection, a common web security vulnerability where attackers can interfere with the queries that an application makes to its database.\nData Handling: It automatically handles the quoting of strings and escaping of special characters, reducing errors in SQL query syntax due to data.\nQuery Efficiency: When running similar queries multiple times, parameterized queries can improve performance as the database engine can reuse the query plan and execution path.\n\nEach ? is a placeholder that is replaced with provided data values in a tuple when the execute method is called. This ensures that the values are properly formatted and inserted into the database, enhancing security and efficiency.\n\n# Insert a row of data\nobs = ('2024-01-02', 37.54, -98.78, 38)\ncursor.execute(\"INSERT INTO soils (date, lat, lon, vwc) VALUES (?,?,?,?)\", obs1)\n\n# Save (commit) the changes\nconn.commit()\n\n\n\nAdd with multiple entries\nYou can insert multiple entries at once using executemany()\n\n# A list of multiple crop records\nnew_data = [('2024-01-02', 36.54, -98.12, 18),\n ('2024-04-14', 38.46, -99.78, 21),\n ('2024-05-23', 38.35, -98.01, 29)]\n\n# Inserting multiple records at a time\ncursor.executemany(\"INSERT INTO soils (date, lat, lon, vwc) VALUES (?,?,?,?)\", new_data)\n\n# Save (commit) the changes\nconn.commit()\n\n# Retrieve all data\ncursor.execute('SELECT * FROM soils;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 38)\n(2, '2024-01-02', 36.54, -98.12, 18)\n(3, '2024-04-14', 38.46, -99.78, 21)\n(4, '2024-05-23', 38.35, -98.01, 29)\n\n\n\n\nQuery data\nTo query specific data from a table in an SQLite database using the SELECT statement, you can specify conditions using the WHERE clause. Here’s a basic syntax:\nSELECT col1, col2, ... FROM table_name WHERE condition1 AND condition2;\nTo execute these queries remember to establish a connection, create a cursor object, execute the query using cursor.execute(query), and then use cursor.fetchall() to retrieve the results. Close the connection to the database once you’re done.\n\n# Retrieve all data\ncursor.execute('SELECT * FROM soils;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 38)\n(2, '2024-01-02', 36.54, -98.12, 18)\n(3, '2024-04-14', 38.46, -99.78, 21)\n(4, '2024-05-23', 38.35, -98.01, 29)\n\n\n\n# Retrieve specific data\ncursor.execute('SELECT date FROM soils WHERE vwc >= 30;')\nfor row in cursor.fetchall():\n print(row)\n\n('2024-01-02',)\n\n\n\n# Retrieve specific data (note that we need spacify the date as a string)\ncursor.execute('SELECT lat,lon FROM soils WHERE date == \"2024-03-07\";')\nfor row in cursor.fetchall():\n print(row)\n\n\ncursor.execute('SELECT * FROM soils WHERE vwc >=30 AND vwc < 40;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 38)\n\n\n\n\nModify data\nYou can update records that match certain criteria.\n\n# Update quantity for specific date note that this will update both rows with the same date)\ncursor.execute(\"UPDATE soils SET vwc = 15 WHERE date = '2024-01-02';\")\n\n# Save (commit) the changes\nconn.commit()\n\n\n\n\n\n\n\nUpdate SQLite\n\n\n\nNote that the previous command updates both rows with the same date. To only specify a single row we need to be more specific in our command.\n\n\n\n# Retrieve all data\ncursor.execute('SELECT * FROM soils;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 15)\n(2, '2024-01-02', 36.54, -98.12, 15)\n(3, '2024-04-14', 38.46, -99.78, 21)\n(4, '2024-05-23', 38.35, -98.01, 29)\n\n\n\n# Update quantity for specific date note that this will update both rows with the same date)\ncursor.execute(\"UPDATE soils SET vwc = 5 WHERE date = '2024-01-02' AND id = 2;\")\n\n# Save (commit) the changes\nconn.commit()\n\n# Retrieve all data\ncursor.execute('SELECT * FROM soils;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 15)\n(2, '2024-01-02', 36.54, -98.12, 5)\n(3, '2024-04-14', 38.46, -99.78, 21)\n(4, '2024-05-23', 38.35, -98.01, 29)\n\n\n\n\nRemove data\nTo remove records, use the DELETE statement.\n\n# Delete the Wheat record\ncursor.execute(\"DELETE FROM soils WHERE id = 3\")\n\n# Save (commit) the changes\nconn.commit()\n\n# Retrieve all data\ncursor.execute('SELECT * FROM soils;')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 15)\n(2, '2024-01-02', 36.54, -98.12, 5)\n(4, '2024-05-23', 38.35, -98.01, 29)\n\n\n\n\nAdd new column/header\n\n# Add a new column\n# Use the DEFAULT construction like: DEFAULT 'Unknown' \n# to populate new column with custom value\ncursor.execute(\"ALTER TABLE soils ADD COLUMN soil_type TEXT;\")\n\n# Retrieve data one more time before we close the database\ncursor.execute('SELECT * FROM soils')\nfor row in cursor.fetchall():\n print(row)\n\n(1, '2024-01-02', 37.54, -98.78, 15, None)\n(2, '2024-01-02', 36.54, -98.12, 5, None)\n(4, '2024-05-23', 38.35, -98.01, 29, None)\n\n\n\n\nClosing the connection\nOnce done with the operations, close the connection to the database.\n\nconn.close()" + }, + { + "objectID": "basic_concepts/sql_database.html#use-pandas-to-set-up-database", + "href": "basic_concepts/sql_database.html#use-pandas-to-set-up-database", + "title": "32  SQLite Database", + "section": "Use Pandas to set up database", + "text": "Use Pandas to set up database\nFor this exercise we will use a table of sorghum yields for Franklin county, KS obtained in 2023. The dataset contains breeder brand, hybrid name, yield, moisture, and total weight. The spreadsheet contains metadata on the first line (which we are going to skip) and in the last few rows. From these last rows, we will use functions to match strings and retrieve the planting and harvest dates of the trial. We will then add this information to the dataframe, and then create an SQLite database.\nPandas provides all the methods to read, clean, and export the Dataframe to a SQLite database.\n\n# Import modules\nimport pandas as pd\nimport sqlite3\n\n\ndf = pd.read_csv('../datasets/sorghum_franklin_county_2023.csv',\n skiprows=[0,1,3])\n# Inspect first few rows\ndf.head()\n\n\n\n\n\n\n\n\nBRAND\nNAME\nYIELD\nPAVG\nMOIST\nTW\n\n\n\n\n0\nPOLANSKY\n5719\n137.2\n107.8\n14.7\n59.1\n\n\n1\nDYNA-GRO\nM60GB88\n135.3\n106.3\n14.0\n57.9\n\n\n2\nDYNA-GRO\nGX22936\n134.7\n105.8\n13.9\n58.7\n\n\n3\nPOLANSKY\n5522\n132.3\n103.9\n13.9\n58.4\n\n\n4\nDYNA-GRO\nGX22932\n131.8\n103.6\n14.5\n59.1\n\n\n\n\n\n\n\n\n# Inspect last few rows\ndf.tail(10)\n\n\n\n\n\n\n\n\nBRAND\nNAME\nYIELD\nPAVG\nMOIST\nTW\n\n\n\n\n18\nDYNA-GRO\nM67GB87\n120.5\n94.7\n13.9\n56.1\n\n\n19\nDYNA-GRO\nM59GB94\n117.7\n92.5\n13.8\n57.7\n\n\n20\nNaN\nAVERAGE\n127.2\n100.0\n14.1\n58.3\n\n\n21\nNaN\nCV (%)\n8.4\n8.4\n0.3\n0.8\n\n\n22\nNaN\nLSD (0.05)\n5.2\n4.1\n0.3\n0.3\n\n\n23\n*Yields must differ by more than the LSD value...\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n24\ndifferent.\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n25\nPlanted 5-24-23\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n26\nHarvested 11-15-23\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n27\nFertility 117-38-25-20 Strip till\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n\n\n\n\n\n\ndf.dropna(subset='BRAND', inplace=True) \ndf.tail(10)\n\n\n\n\n\n\n\n\nBRAND\nNAME\nYIELD\nPAVG\nMOIST\nTW\n\n\n\n\n15\nDYNA-GRO\nM63GB78\n122.7\n96.4\n13.9\n58.0\n\n\n16\nDYNA-GRO\nGX22937\n121.3\n95.4\n14.2\n58.4\n\n\n17\nDYNA-GRO\nGX22923\n121.2\n95.2\n13.7\n55.8\n\n\n18\nDYNA-GRO\nM67GB87\n120.5\n94.7\n13.9\n56.1\n\n\n19\nDYNA-GRO\nM59GB94\n117.7\n92.5\n13.8\n57.7\n\n\n23\n*Yields must differ by more than the LSD value...\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n24\ndifferent.\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n25\nPlanted 5-24-23\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n26\nHarvested 11-15-23\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n27\nFertility 117-38-25-20 Strip till\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n\n\n\n\n\n\n# Extract planting date\nidx = df['BRAND'].str.contains(\"Planted\")\nplanting_date_str = df.loc[idx, 'BRAND'].values[0]\nplanting_date = planting_date_str.split(' ')[1]\nprint(planting_date)\n\n5-24-23\n\n\n\n# Extract harvest date\nidx = df['BRAND'].str.contains(\"Harvested\")\nharvest_date_str = df.loc[idx, 'BRAND'].values[0]\nharvest_date = harvest_date_str.split(' ')[1]\nprint(harvest_date)\n\n11-15-23\n\n\n\n# Once we are done extracting metadata, let's remove the last few rows\ndf = df.iloc[:-5]\n\n\n# Convert header names to lower case to avoid conflict with SQL syntax\ndf.rename(str.lower, axis='columns', inplace=True)\ndf.head()\n\n\n\n\n\n\n\n\nbrand\nname\nyield\npavg\nmoist\ntw\nplanting_date\nharvest_date\n\n\n\n\n0\nPOLANSKY\n5719\n137.2\n107.8\n14.7\n59.1\n5-24-23\n11-15-23\n\n\n1\nDYNA-GRO\nM60GB88\n135.3\n106.3\n14.0\n57.9\n5-24-23\n11-15-23\n\n\n2\nDYNA-GRO\nGX22936\n134.7\n105.8\n13.9\n58.7\n5-24-23\n11-15-23\n\n\n3\nPOLANSKY\n5522\n132.3\n103.9\n13.9\n58.4\n5-24-23\n11-15-23\n\n\n4\nDYNA-GRO\nGX22932\n131.8\n103.6\n14.5\n59.1\n5-24-23\n11-15-23\n\n\n\n\n\n\n\n\n# Add planting and harvest date to Dataframe to make it more complete\ndf['planting_date'] = planting_date\ndf['harvest_date'] = harvest_date\ndf.head()\n\n\n\n\n\n\n\n\nbrand\nname\nyield\npavg\nmoist\ntw\nplanting_date\nharvest_date\n\n\n\n\n0\nPOLANSKY\n5719\n137.2\n107.8\n14.7\n59.1\n5-24-23\n11-15-23\n\n\n1\nDYNA-GRO\nM60GB88\n135.3\n106.3\n14.0\n57.9\n5-24-23\n11-15-23\n\n\n2\nDYNA-GRO\nGX22936\n134.7\n105.8\n13.9\n58.7\n5-24-23\n11-15-23\n\n\n3\nPOLANSKY\n5522\n132.3\n103.9\n13.9\n58.4\n5-24-23\n11-15-23\n\n\n4\nDYNA-GRO\nGX22932\n131.8\n103.6\n14.5\n59.1\n5-24-23\n11-15-23\n\n\n\n\n\n\n\n\n# Use Pandas to turn DataFrame into a SQL Database\n\n# Connect to SQLite database (if it doesn't exist, it will be created)\nconn = sqlite3.connect('sorghum_trial.db')\n\n# Write the data to a sqlite table\ndf.to_sql('sorghum_trial', conn, index=False, if_exists='replace') # to overwrite use option if_exists='replace'\n\n# Close the connection\nconn.close()\n\n\nConnect, access all data, and close database\n\n# Connect to SQLite database (if it doesn't exist, it will be created)\nconn = sqlite3.connect('sorghum_trial.db')\n\n# Create cursor\ncursor = conn.cursor()\n\n# Access all data\ncursor.execute('SELECT * FROM sorghum_trial')\nfor row in cursor.fetchall():\n print(row)\n \n# Access all data\nprint('') # Add some white space\ncursor.execute('SELECT brand, name FROM sorghum_trial WHERE yield > 130')\nfor row in cursor.fetchall():\n print(row)\n \n# Close the connection\nconn.close()\n\n('POLANSKY', '5719', 137.2, 107.8, 14.7, 59.1, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M60GB88', 135.3, 106.3, 14.0, 57.9, '5-24-23', '11-15-23')\n('DYNA-GRO', 'GX22936', 134.7, 105.8, 13.9, 58.7, '5-24-23', '11-15-23')\n('POLANSKY', '5522', 132.3, 103.9, 13.9, 58.4, '5-24-23', '11-15-23')\n('DYNA-GRO', 'GX22932', 131.8, 103.6, 14.5, 59.1, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M72GB71', 130.9, 102.9, 14.5, 59.0, '5-24-23', '11-15-23')\n('MATURITY CHECK', 'MED', 128.7, 101.1, 14.0, 58.2, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M71GR91', 128.7, 101.1, 14.4, 59.3, '5-24-23', '11-15-23')\n('PIONEER', '86920', 128.1, 100.7, 13.9, 57.9, '5-24-23', '11-15-23')\n('MATURITY CHECK', 'EARLY', 127.9, 100.5, 14.2, 58.8, '5-24-23', '11-15-23')\n('POLANSKY', '5629', 126.5, 99.4, 13.8, 57.2, '5-24-23', '11-15-23')\n('MATURITY CHECK', 'LATE', 126.2, 99.2, 14.2, 58.3, '5-24-23', '11-15-23')\n('PIONEER', '84980', 125.5, 98.6, 14.1, 58.8, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M60GB31', 124.9, 98.2, 14.2, 59.2, '5-24-23', '11-15-23')\n('DYNA-GRO', 'GX22934', 122.8, 96.5, 14.6, 59.5, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M63GB78', 122.7, 96.4, 13.9, 58.0, '5-24-23', '11-15-23')\n('DYNA-GRO', 'GX22937', 121.3, 95.4, 14.2, 58.4, '5-24-23', '11-15-23')\n('DYNA-GRO', 'GX22923', 121.2, 95.2, 13.7, 55.8, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M67GB87', 120.5, 94.7, 13.9, 56.1, '5-24-23', '11-15-23')\n('DYNA-GRO', 'M59GB94', 117.7, 92.5, 13.8, 57.7, '5-24-23', '11-15-23')\n\n('POLANSKY', '5719')\n('DYNA-GRO', 'M60GB88')\n('DYNA-GRO', 'GX22936')\n('POLANSKY', '5522')\n('DYNA-GRO', 'GX22932')\n('DYNA-GRO', 'M72GB71')" + }, + { + "objectID": "exercises/meteogram.html#estimate-some-useful-metrics", + "href": "exercises/meteogram.html#estimate-some-useful-metrics", + "title": "33  Meteogram", + "section": "Estimate some useful metrics", + "text": "Estimate some useful metrics\nTo characterize what happened during the entire year, let’s compute the annual rainfall, maximum wind speed, and maximum and minimum air temperature.\n\n# Find and print total precipitation\nP_total = df['prcp'].sum().round(2)\nprint(f'Total precipitation in 2023 was {P_total} mm')\n\nTotal precipitation in 2023 was 523.28 mm\n\n\n\n# Find the total number of days with measurable precipitation\nP_hours = (df['prcp'] > 0).sum()\nprint(f'There were {P_hours} days with precipitation')\n\nThere were 108 days with precipitation\n\n\n\n# Find median air temperature. Print value.\nTmedian = df['tavg'].median()\nprint(f'Median air temperature was {Tmedian} Celsius')\n\nMedian air temperature was 13.65 Celsius\n\n\n\n# Find value and time of minimum air temperature. Print value and timestamp.\nfmt = '%A, %B %d, %Y'\nTmin_idx = df['tmin'].argmin()\nTmin_value = df.loc[Tmin_idx, 'tmin']\nTmin_timestamp = df.loc[Tmin_idx, 'datetime']\nprint(f'The lowest air temperature was {Tmin_value} on {Tmin_timestamp:{fmt}}')\n\nThe lowest air temperature was -22.4 on Thursday, December 22, 2022\n\n\n\n# Find value and time of maximum air temperature. Print value and timestamp.\nTmax_idx = df['tmax'].argmax()\nTmax_value = df.loc[Tmax_idx, 'tmax']\nTmax_timestamp = df.loc[Tmax_idx, 'datetime']\nprint(f'The highest air temperature was {Tmax_value} on {Tmax_timestamp:{fmt}}')\n\nThe highest air temperature was 37.4 on Saturday, July 23, 2022\n\n\n\n# Find max wind gust and time of occurrence. Print value and timestamp.\nWmax_idx = df['wspd'].argmax()\nWmax_value = df.loc[Wmax_idx, 'wspd']\nWmax_timestamp = df.loc[Wmax_idx, 'datetime']\nprint(f'The highest wind speed was {Wmax_value:.2f} m/s on {Wmax_timestamp:{fmt}}')\n\nThe highest wind speed was 8.15 m/s on Tuesday, April 12, 2022" + }, + { + "objectID": "exercises/meteogram.html#meteogram", + "href": "exercises/meteogram.html#meteogram", + "title": "33  Meteogram", + "section": "Meteogram", + "text": "Meteogram\n\n# Create meteogram plot\n\n# Define style\nplt.style.use('ggplot')\n\n# Define fontsize\nfont = 14\n\n# Create plot\nplt.figure(figsize=(14,30))\n\n# Air temperature\nplt.subplot(9,1,1)\nplt.title('Kings Creek Meteogram for 2022', size=20)\nplt.plot(df['datetime'], df['tmin'], color='navy')\nplt.plot(df['datetime'], df['tmax'], color='tomato')\nplt.ylabel('Air Temperature (°C)', size=font)\nplt.yticks(size=font)\n\n# Relative humidity\nplt.subplot(9,1,2)\nplt.plot(df['datetime'], df['rmin'], color='navy')\nplt.plot(df['datetime'], df['rmax'], color='tomato')\nplt.ylabel('Relative Humidity (%)', size=font)\nplt.yticks(size=font)\nplt.ylim(0,100)\n\n# Atmospheric pressure\nplt.subplot(9,1,3)\nplt.plot(df['datetime'], df['pressure'], '-k')\nplt.ylabel('Pressure (kPa)', size=font)\nplt.yticks(size=font)\n\n# Vapor pressure deficit\nplt.subplot(9,1,4)\nplt.plot(df['datetime'], df['vpd'], '-k')\nplt.ylabel('Vapor pressure deficit (kPa)', size=font)\nplt.yticks(size=font)\n\n# Wind speed\nplt.subplot(9,1,5)\nplt.plot(df['datetime'], df['wspd'], '-k', label='Wind speed')\nplt.ylabel('Wind Speed ($m \\ s^{-1}$)', size=font)\nplt.legend(loc='upper left')\nplt.yticks(size=font)\n\n# Solar radiation\nplt.subplot(9,1,6)\nplt.plot(df['datetime'], df['srad'])\nplt.ylabel('Solar radiation ($W \\ m^{-2}$)', size=font)\nplt.yticks(size=font)\n\n# Precipitation\nplt.subplot(9,1,7)\nplt.step(df['datetime'], df['prcp'], color='navy')\nplt.ylabel('Precipitation (mm)', size=font)\nplt.yticks(size=font)\nplt.ylim(0.01,35)\nplt.text(df['datetime'].iloc[5], 30, f\"Total = {P_total} mm\", size=14)\n\n# Soil temperature\nplt.subplot(9,1,8)\nplt.plot(df['datetime'], df['soiltemp_5cm'], '-k', label='5 cm')\nplt.plot(df['datetime'], df['soiltemp_20cm'], '-g', label='20 cm')\nplt.plot(df['datetime'], df['soiltemp_40cm'], '--r', label='40 cm')\nplt.ylabel('Soil temperature (°C)', size=font)\nplt.yticks(size=font)\nplt.ylim(df['soiltemp_5cm'].min()-10, df['soiltemp_5cm'].max()+10)\nplt.grid(which='minor')\nplt.legend(loc='upper left')\n\n# Soil moisture\nplt.subplot(9,1,9)\nplt.plot(df['datetime'], df['vwc_5cm'], '-k', label='5 cm')\nplt.plot(df['datetime'], df['vwc_20cm'], '-g', label='20 cm')\nplt.plot(df['datetime'], df['vwc_40cm'], '--r', label='40 cm')\nplt.ylabel('Soil Moisture ($m^3 \\ m^{-3}$)', size=font)\nplt.yticks(size=font)\nplt.ylim(0, 0.5)\nplt.grid(which='minor')\nplt.legend(loc='best')\n\nplt.subplots_adjust(hspace=0.2) # for space between columns wspace=0)\n#plt.savefig('meteogram.svg', format='svg')\nplt.show()" + }, + { + "objectID": "exercises/group_least_variance.html#practice", + "href": "exercises/group_least_variance.html#practice", + "title": "34  Group with least variance", + "section": "Practice", + "text": "Practice\n\nConvert the steps of the script into a function. Make sure to add a docstring and describe the inputs of the function.\nCan you find and measure some random objects around you and find the most uniform set of k members of the set? You could measure height, area, volume, mass, or any other property or variable." + }, + { + "objectID": "exercises/random_plots.html#complete-randomized-design", + "href": "exercises/random_plots.html#complete-randomized-design", + "title": "35  Random plot generator", + "section": "Complete randomized design", + "text": "Complete randomized design\n\n# Randomized complete block\nnp.random.seed(1)\n\n# Create the experimental units\nexp_units_crd = np.repeat(treatments, replicates)\n\n# Randomize the order of the experimental units (inplace operation)\nnp.random.shuffle(exp_units_crd)\n\n# Divide array into individual replicates for easier reading\nexp_units_crd = np.reshape(exp_units_crd, (replicates,treatments.shape[0]))\nprint(exp_units_crd)\n\n[['N50' 'N50' 'N100' 'N0' 'N200']\n ['N50' 'N100' 'N100' 'N0' 'N0']\n ['N200' 'N25' 'N25' 'N200' 'N0']\n ['N100' 'N0' 'N100' 'N200' 'N200']\n ['N25' 'N25' 'N50' 'N50' 'N25']]" + }, + { + "objectID": "exercises/random_plots.html#complete-randomized-block", + "href": "exercises/random_plots.html#complete-randomized-block", + "title": "35  Random plot generator", + "section": "Complete randomized block", + "text": "Complete randomized block\n\n# Complete randomized\nnp.random.seed(1)\n\n# Create array of experimental units\n# In field experiments it is common to keep the first block sorted\n# to have as a reference during field visits. This is not a problem\n# since the sorted array is one of the possible block arrangements\nexp_units_rcbd = treatments\n\nfor i in range(replicates-1):\n \n # Draw treatments without replacement\n block = np.random.choice(treatments, size=treatments.size, replace=False)\n exp_units_rcbd = np.vstack((exp_units_rcbd, block))\n\nprint(exp_units_rcbd)\n\n[['N0' 'N25' 'N50' 'N100' 'N200']\n ['N50' 'N25' 'N200' 'N0' 'N100']\n ['N0' 'N50' 'N200' 'N100' 'N25']\n ['N50' 'N100' 'N25' 'N200' 'N0']\n ['N100' 'N0' 'N25' 'N50' 'N200']]" + }, + { + "objectID": "exercises/random_plots.html#latin-square-design", + "href": "exercises/random_plots.html#latin-square-design", + "title": "35  Random plot generator", + "section": "Latin square design", + "text": "Latin square design\n\n# Latin square\nnp.random.seed(1)\n\n# Create experimental units, all rows with the same treatments\nexp_units_latsq = np.tile(treatments,replicates).reshape((replicates,treatments.size))\n\n# Create array with random shift steps\nshifts = np.random.choice(range(replicates), size=replicates, replace=False)\n\nfor n,k in enumerate(shifts):\n \n # Shift first block by a random step\n exp_units_latsq[n] = np.roll(exp_units_latsq[n], k)\n\nprint(exp_units_latsq)\n\n[['N100' 'N200' 'N0' 'N25' 'N50']\n ['N200' 'N0' 'N25' 'N50' 'N100']\n ['N25' 'N50' 'N100' 'N200' 'N0']\n ['N0' 'N25' 'N50' 'N100' 'N200']\n ['N50' 'N100' 'N200' 'N0' 'N25']]" + }, + { + "objectID": "exercises/random_plots.html#practice", + "href": "exercises/random_plots.html#practice", + "title": "35  Random plot generator", + "section": "Practice", + "text": "Practice\n\nPractice by coding other experimental designs, like split-plot design." + }, + { + "objectID": "exercises/random_plots.html#references", + "href": "exercises/random_plots.html#references", + "title": "35  Random plot generator", + "section": "References", + "text": "References\nJayaraman, K., 1984. FORSPA-FAO Publication A Statistical Manual for forestry Research (No. Fe 25). FAO,. http://www.fao.org/3/X6831E/X6831E07.htm" + }, + { + "objectID": "exercises/mixing_model.html#example-1-tank-level-and-salt-concentration", + "href": "exercises/mixing_model.html#example-1-tank-level-and-salt-concentration", + "title": "36  Mixing problems", + "section": "Example 1: Tank level and salt concentration", + "text": "Example 1: Tank level and salt concentration\nA 1500 gallon tank initially contains 600 gallons of water with 5 lbs of salt dissolved in it. Water enters the tank at a rate of 9 gal/hr and the water entering the tank has a salt concentration of \\frac{1}{5}(1 + cos(t)) lbs/gal. If a well-mixed solution leaves the tank at a rate of 6 gal/hr:\n\nhow long does it take for the tank to overflow?\nhow much salt (total amount in lbs) is in the entire tank when it overflows?\n\nAssume each iteration is equivalent to one hour\nWe will break the problem into two steps. The first step consists of focusing on the tank volume and leaving the calculation of the salt concentration aside. Trying to solve both questions at the same time can make this problem more difficult than it actual is. A good way of thinking about this problem is by making an analogy with the balance of a checking account (tank level), where we have credits (inflow rate, salary) and debits (outflow, expenses).\n\nStep 1: Find time it takes to fill the tank\n\n# Initial parameters\ntank_capacity = 1500 # gallons\ntank_level = 600 # initial tank_level in gallons\ninflow_rate = 9 # gal/hr\noutflow_rate = 6 # gal/hr\n\n\n# Step 1: Compute tank volume and determine when the tank is full\ncounter_hours = 0\nwhile tank_level < tank_capacity:\n tank_level = tank_level + inflow_rate - outflow_rate \n counter_hours += 1\n \nprint('Hours:', counter_hours)\nprint('Tank level:',tank_level)\n\nHours: 300\nTank level: 1500\n\n\n\n\nStep 2: Add the calculation of the amount of salt\nNow that we understand the problem in simple terms and we were able to implement it in Python, is time to add the computation of salt concentration at each time step. In this step is important to realize that concentration is amount of salt per unit volume of water, in this case gallons of water. Following the same reasoning of the previous step, we now need to calculate the balance of salt taking into account initial salt content, inflow, and outflow. So, to solve the problem we need:\n\nthe inflow rate of water with salt\nthe salt concentration of the inflow rate\nthe outflow rate of water with salt\nthe salt concentration of the outflow rate (we need to calculate this)\n\nFrom the statement we have the first 3 pieces of information, but we lack the last one. Since concetration is mass of salt per unit volume of water, we just need to divide the total amount of salt over the current volume of water in the tank. So at the beginning we have 5 lbs/600 gallons = 0.0083 lbs/gal, which will be the salt concentration of the outflow during the first hour. Becasue the amount of water and salt in the tank changes every hour, we need to include this computation in each iteration to update the salt concentration of the outflow.\n\n# Initial parameters\nt = 0\ntank_level = np.ones(period)*np.nan # Pre-allocate array with NaNs\nsalt_mass = np.ones(period)*np.nan # Pre-allocate array with NaNs\ntank_level[0] = 600 # gallons\nsalt_mass[0] = 5 # lbs\ntank_capacity = 1500 # gallons\ninflow_rate = 9 # gal/hr\noutflow_rate = 6 # gal/hr\n\n# Compute tank volume and salt mass at time t until tank is full\nwhile tank_level[t] < tank_capacity:\n \n # Add one hour\n t += 1\n \n # The salt concentration will be computed using the tank level of the previous hour\n salt_inflow = 1/5*(1+np.cos(t)) * inflow_rate # lbs/gal ranges between 0 and 0.4\n salt_outflow = salt_mass[t-1]/tank_level[t-1] * outflow_rate\n salt_mass[t] = salt_mass[t-1] + salt_inflow - salt_outflow\n \n # Now we can update the tank level\n tank_level[t] = tank_level[t-1] + inflow_rate - outflow_rate # volume of the tank\n\nprint(t, 'hours')\nprint(np.round(salt_mass[t]),'lbs of salt')\n\n300 hours\n280.0 lbs of salt\n\n\n\n# Create figures\nplt.figure(figsize=(4,12))\n\nplt.subplot(3,1,1)\nplt.plot(range(period),tank_level)\nplt.xlabel('Hours')\nplt.ylabel('Tank level (gallons)')\n\nplt.subplot(3,1,2)\nplt.plot(range(period),salt_mass)\nplt.xlabel('Hours')\nplt.ylabel('Salt mass in the tank (lbs)')\n\nplt.subplot(3,1,3)\n# Plot every 5 values to clearly see the curve in the figure\nplt.plot(range(0,period,5), 1/5*(1+np.cos(range(0, period,5))))\nplt.xlabel('Hours')\nplt.ylabel('Inflow sal concentration (lbs/gal)')\nplt.show()\n\n\n\n\n\n\nExample 2: Excess of herbicide problem\nA farmer is preparing to control weeds in a field crop using a sprayer with a tank containing 100 liters of fresh water. The recommended herbice concentration to control the weeds without affecting the crop is 2%, which means that the farmer would need to add 2 liters of herbice to the tank. However, due to an error while measuring the herbicide, the farmer adds 3 liters of herbicide instead of 2 liters, which will certainly kill the weeds, but may also damage the crop and contaminate the soil with unnecesary product. To fix the problem and avoid disposing the entire tank, the farmer decides to open the outflow valve to let some herbicide solution out of the tank at a rate of 3 liters per minute, while at the same time, adding fresh water from the top of the tank at a rate of 3 liters per minute. Assume that the tank has a stirrer that keeps the solution well-mixed (i.e. the herbicide concentration at any given time is homogenous across the tank).\nIndicate the time in minutes at which the herbicide concentration in the tank is restored at 2% (0.02 liters of herbicede per liter of fresh water). In other words, you need to find the time at which the farmer needs to close the outflow valve.\n\n# Numerical Solution\ntank_level = 100 # Liters\nchemical_volume = 3 # Liters\nchemical_concentration = chemical_volume/tank_level # Liter of chemical per Liter of water\ninflow_rate = 3 # Liters per minute\noutflow_rate = 3 # Liters per minute\nrecommended_concentration = 0.02 # Liter of chemical per Liter of water\ndt = 0.1 # Time of each iteration in minutes\ncounter = 0 # Time tracker in minutes\n\nwhile chemical_concentration > recommended_concentration:\n tank_level = tank_level + inflow_rate*dt - outflow_rate*dt\n chemical_inflow = 0\n chemical_outflow = chemical_volume/tank_level*outflow_rate*dt\n chemical_volume = chemical_volume + chemical_inflow - chemical_outflow\n chemical_concentration = chemical_volume/tank_level\n counter += dt\n\nprint('Solution:',round(counter,1),'minutes')\n\nSolution: 13.5 minutes" + }, + { + "objectID": "exercises/mixing_model.html#references", + "href": "exercises/mixing_model.html#references", + "title": "36  Mixing problems", + "section": "References", + "text": "References\nThe examples in this notebook were adapted from problems and exercises in Morris. Tenenbaum and Pollard, H., 1963. Ordinary differential equations: an elementary textbook for students of mathematics, engineering, and the sciences. Dover Publications." + }, + { + "objectID": "exercises/mass_volume_relationships.html#problem-1", + "href": "exercises/mass_volume_relationships.html#problem-1", + "title": "37  Mass-volume relationships", + "section": "Problem 1", + "text": "Problem 1\nA cylindrical soil sample with a diameter of 5.00 cm and a height of 5.00 cm has a wet mass of 180.0 g. After oven-drying the soil sample at 105 degrees Celsius for 48 hours, the sample has a dry mass of 147.0 g. Based on the provided information, calculate:\n\nvolume of the soil sample: V_t = \\pi \\ r^2 \\ h \nmass of water in the sample: M_{water} = M_{wet \\ soil} - M_{dry \\ soil} \nbulk density: \\rho_b = M_{dry \\ soil}/V_t \nporosity: f = 1 - \\rho_b/\\rho_p \ngravimetric water content: \\theta_g = M_{water} / M_{dry \\ soil} \nvolumetric water content: \\theta_v = V_{water} / V_{t} \nrelative saturation: \\theta_rel = \\theta_v / f \nsoil water storage expressed in mm and inches of water: S = \\theta_v * z \n\nThroughout the exercise, assume a particle density of 2.65 g cm^{-3} and that water has a density of 0.998 g cm^{-3}\n\n# Import modules\nimport numpy as np\n\n\n# Problem information\nsample_diameter = 5.00 # cm\nsample_height = 5.00 # cm\nwet_mass = 180.0 # grams\ndry_mass = 147.0 # grams\ndensity_water = 0.998 # g/cm^3 at 20 Celsius\nparticle_density = 2.65 # g/cm^3\n\n\n# Sample volume\nsample_volume = np.pi * (sample_diameter/2)**2 * sample_height\nprint(f'Volume of sample is: {sample_volume:.0f} cm^3')\n\nVolume of sample is: 98 cm^3\n\n\n\n# Mass of water in the sample\nmass_water = wet_mass - dry_mass\nprint(f'Mass of water is: {mass_water:.0f} g')\n\nMass of water is: 33 g\n\n\n\n# Bulk density\nbulk_density = dry_mass/sample_volume\nprint('Bulk density of the sample is:', round(bulk_density,2), 'g/cm^3')\n\nBulk density of the sample is: 1.5 g/cm^3\n\n\n\n# Porosity\nf = (1 - bulk_density/particle_density)\nprint(f'Porosity of the sample is: {f:.2f}') # Second `f` is for floating-point\n\nPorosity of the sample is: 0.43\n\n\n\n# Gravimetric soil mositure \n# Mass of water per unit mass of dry soil. Typically in g/g or kg/kg\ntheta_g = mass_water / dry_mass\nprint(f'Gravimetric water content is: {theta_g:.3f} g/g')\n\nGravimetric water content is: 0.224 g/g\n\n\n\n# Volumetric soil mositure\n# Volume of water per unit volume of dry soil. Typically in cm^3/cm^3 or m^3/m^3\nvolume_water = mass_water / density_water\ntheta_v = volume_water / sample_volume\nprint(f'Volumetric water content is: {theta_v:.3f} cm^3/cm^3')\n\nVolumetric water content is: 0.337 cm^3/cm^3\n\n\n\n# Relative saturation\nrel_sat = theta_v/f\nprint(f'Relative saturation is: {rel_sat:.2f}')\n\nRelative saturation is: 0.77\n\n\n\n# Storage\nstorage_mm = theta_v * sample_height*10 # convert from cm to mm\nstorage_in = storage_mm/25.4 # 1 inch = 25.4 mm\nprint(f'The soil water storage in mm is: {storage_mm:.1f} mm')\nprint(f'The soil water storage in inches is: {storage_in:.3f} inches')\n\nThe soil water storage in mm is: 16.8 mm\nThe soil water storage in inches is: 0.663 inches" + }, + { + "objectID": "exercises/mass_volume_relationships.html#problem-2", + "href": "exercises/mass_volume_relationships.html#problem-2", + "title": "37  Mass-volume relationships", + "section": "Problem 2", + "text": "Problem 2\nHow many liters of water are stored in the top 1 meter of the soil profile of a field that has an area of 64 hectares (about 160 acres)? Assume that average soil moisture of the field is the volumetric water content computed in the previous problem.\n\n\n\n\n\n\nNote\n\n\n\nNote Recall that the volume ratio is the same as the length ratio. This means that we can use the volumetric water content to represent the ‘height of water’ in the field. If you are having trouble visualizing this fact, grab a cylindrical container and fill it with 25% of its volume in water. Then measure the height of the water relative to the height of the container, it should also be 25%. Note that this will not work for conical shapes, so make sure to grab a uniform shape like a cylinder or a cube.\n\n\n\n# Liters of water in a field\nfield_area = 64*10_000 # 1 hectare = 10,000 m^2\nprofile_length = 1 # meter\nequivalent_height_of_water = profile_length * theta_v # m of water\nvolume_of_water = field_area * equivalent_height_of_water # m^3 of water\n\n# Use the fact that 1 m^3 = 1,000 liters\nliters_of_water = volume_of_water * 1_000\nprint(f'There are {round(liters_of_water)} liters of water')\n\n# Compare volume of water to an Olympic-size swimming pool (50 m x 25 m x 2 m)\npool_volume = 50 * 25 * 2 # m^3\nprint(f'This is equivalent to {round(liters_of_water/pool_volume)} olympic swimming pools!')\n\nThere are 215557669 liters of water\nThis is equivalent to 86223 olympic swimming pools!" + }, + { + "objectID": "exercises/mass_volume_relationships.html#problem-3", + "href": "exercises/mass_volume_relationships.html#problem-3", + "title": "37  Mass-volume relationships", + "section": "Problem 3", + "text": "Problem 3\nImagine that we want to change the soil texture of this field in the rootzone (top 1 m). How much sand, silt, or clay do we need to haul in to change the textural composition by say 1%? While different soil fractions usually have slightly different bulk densities, let’s use the value from the previous problem. We are not looking for the exact number, we just want a ballpark idea. So, what is 1% of the total mass of the field considering the top 1 m of the soil profile?\n\n# Field area was already in converted from hectares to m^2\nfield_volume = field_area * 1 # m^3\n\n# Since 1 Mg/m^3 = g/cm^3, we use the same value (1 Megagram = 1 metric ton)\nfield_mass = field_volume * bulk_density \n\none_percent_mass = field_mass/100\nprint(f'1% of the entire field mass is {one_percent_mass:.1f} Mg')\n\nultra_truck_maxload = 450 # Mg or metric tons\nnumber_of_truck_loads_required = one_percent_mass / ultra_truck_maxload\nprint(f'It would require the load of {number_of_truck_loads_required:.0f} trucks')\n\n1% of the entire field mass is 9582.9 Mg\nIt would require the load of 21 trucks" + }, + { + "objectID": "exercises/soil_textural_class.html#references", + "href": "exercises/soil_textural_class.html#references", + "title": "38  Soil textural classes", + "section": "References", + "text": "References\n\nBenham E., Ahrens, R.J., and Nettleton, W.D. (2009). Clarification of Soil Texture Class Boundaries. Nettleton National Soil Survey Center, USDA-NRCS, Lincoln, Nebraska.\nYuji Ikeda. (2023). yuzie007/mpltern: 1.0.2 (1.0.2). Zenodo. https://doi.org/10.5281/zenodo.8289090" + }, + { + "objectID": "exercises/distribution_daily_precipitation.html#read-and-prepare-dataset-for-analysis", + "href": "exercises/distribution_daily_precipitation.html#read-and-prepare-dataset-for-analysis", + "title": "39  Distribution daily precipitation", + "section": "Read and prepare dataset for analysis", + "text": "Read and prepare dataset for analysis\n\n# Load data\nfilename = '../datasets/Greeley_Kansas.csv'\ndf = pd.read_csv(filename, parse_dates=['timestamp'])\n\n# Check first few rows\ndf.head(3)\n\n\n\n\n\n\n\n\nid\nlongitude\nlatitude\ntimestamp\ndoy\npr\nrmax\nrmin\nsph\nsrad\n...\ntmmn\ntmmx\nvs\nerc\neto\nbi\nfm100\nfm1000\netr\nvpd\n\n\n\n\n0\n19800101\n-101.805968\n38.480534\n1980-01-01\n1\n2.942802\n89.670753\n54.058212\n0.002494\n7.778843\n...\n-7.795996\n3.151758\n2.893231\n15.399130\n0.831915\n-3.175810e-07\n20.635117\n18.033571\n1.249869\n0.193092\n\n\n1\n19800102\n-101.805968\n38.480534\n1980-01-02\n2\n0.446815\n100.000000\n52.695320\n0.003245\n5.499405\n...\n-5.106787\n1.702234\n2.518820\n21.250834\n0.520971\n2.130799e+01\n20.802979\n18.478359\n0.691624\n0.085504\n\n\n2\n19800103\n-101.805968\n38.480534\n1980-01-03\n3\n0.000000\n100.000000\n65.851830\n0.002681\n9.102443\n...\n-9.276953\n-0.898444\n2.564172\n21.070301\n0.403031\n2.138025e+01\n21.159216\n18.454714\n0.511131\n0.050106\n\n\n\n\n3 rows × 21 columns\n\n\n\n\n# Add year column, so that we can group events and totals by year\ndf.insert(1, 'year', df['timestamp'].dt.year)\ndf.head(3)\n\n\n\n\n\n\n\n\nid\nyear\nlongitude\nlatitude\ntimestamp\ndoy\npr\nrmax\nrmin\nsph\n...\ntmmn\ntmmx\nvs\nerc\neto\nbi\nfm100\nfm1000\netr\nvpd\n\n\n\n\n0\n19800101\n1980\n-101.805968\n38.480534\n1980-01-01\n1\n2.942802\n89.670753\n54.058212\n0.002494\n...\n-7.795996\n3.151758\n2.893231\n15.399130\n0.831915\n-3.175810e-07\n20.635117\n18.033571\n1.249869\n0.193092\n\n\n1\n19800102\n1980\n-101.805968\n38.480534\n1980-01-02\n2\n0.446815\n100.000000\n52.695320\n0.003245\n...\n-5.106787\n1.702234\n2.518820\n21.250834\n0.520971\n2.130799e+01\n20.802979\n18.478359\n0.691624\n0.085504\n\n\n2\n19800103\n1980\n-101.805968\n38.480534\n1980-01-03\n3\n0.000000\n100.000000\n65.851830\n0.002681\n...\n-9.276953\n-0.898444\n2.564172\n21.070301\n0.403031\n2.138025e+01\n21.159216\n18.454714\n0.511131\n0.050106\n\n\n\n\n3 rows × 22 columns" + }, + { + "objectID": "exercises/distribution_daily_precipitation.html#find-value-and-date-of-largest-daily-rainfall-event-on-record", + "href": "exercises/distribution_daily_precipitation.html#find-value-and-date-of-largest-daily-rainfall-event-on-record", + "title": "39  Distribution daily precipitation", + "section": "Find value and date of largest daily rainfall event on record", + "text": "Find value and date of largest daily rainfall event on record\nTo add some context, let’s find out what is the largest daily rainfall event in the period 1980-2020 for Greeley county, KS.\n\n# Find largest rainfall event and the date\namount_largest_event = df['pr'].max()\nidx_largest_event = df['pr'].argmax()\ndate_largest_event = df.loc[idx_largest_event, 'timestamp']\n\nprint(f'The largest rainfall was {amount_largest_event:.1f} mm')\nprint(f'and occurred on {date_largest_event:%Y-%m-%d}')\n\nThe largest rainfall was 68.6 mm\nand occurred on 2010-05-18" + }, + { + "objectID": "exercises/distribution_daily_precipitation.html#probability-density-function-of-precipitation-amount", + "href": "exercises/distribution_daily_precipitation.html#probability-density-function-of-precipitation-amount", + "title": "39  Distribution daily precipitation", + "section": "Probability density function of precipitation amount", + "text": "Probability density function of precipitation amount\nThe most important step before creating the histogram is to identify days with daily rainfall greater than 0 mm. If we don’t do this, we will include a large number of zero occurrences, that will affect the distribution. We know that it does not rain every day in this region, but when it does, what is the typical size of a rainfall event?\n\n# Boolean to identify days with rainfall greater than 0 mm\nidx_rained = df['pr'] > 0\n\n# For brevity we will create a new variable with all the precipitation events >0 mm\ndata = df.loc[idx_rained,'pr']\n\n\n# Determine median daily rainfall (not considering days without rain)\nmedian_rainfall = data.median()\nprint(f'Median daily rainfall is {median_rainfall:.1f} mm')\n\nMedian daily rainfall is 2.1 mm\n\n\n\n# Fit theoretical distribution function\n# I assumed a lognormal distribution based on the shape of the histogram\n# and the fact that rainfall cannot be negative\nbounds = [(1, 10),(0.1,10),(0,10)] # Guess bounds for `s` parameters of the lognorm pdf\nfitted_pdf = stats.fit(stats.lognorm, data, bounds)\nprint(fitted_pdf.params)\n\n\nFitParams(s=1.5673979274845327, loc=0.23667991099824695, scale=1.6451037468195546)\n\n\n\n# Create vector from 0 to x_max to plot the lognorm pdf\nx = np.linspace(data.min(), data.max(), num=1000)\n\n# Create figure\nplt.figure(figsize=(6,4))\nplt.title('Daily Precipitation Greeley County, KS 1980-2020')\nplt.hist(data, bins=200, density=True,\n facecolor='lightgrey', edgecolor='k', label='Histogram')\nplt.axvline(median_rainfall, linestyle='--', color='k', label='Median')\nplt.plot(x, stats.lognorm.pdf(x, *fitted_pdf.params), color='r', label='lognorm pdf')\nplt.xlabel('Precipitation (mm)')\nplt.ylabel('Density')\nplt.xlim([0, 20])\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/distribution_daily_precipitation.html#cumulative-density-function", + "href": "exercises/distribution_daily_precipitation.html#cumulative-density-function", + "title": "39  Distribution daily precipitation", + "section": "Cumulative density function", + "text": "Cumulative density function\nWe can also ask: What is the probability of having a daily rainfall event equal or lower than x amount? The empirical cumulative dsitribution can help us answer this question. Note that if we ask greater than x amount, we will need to use the complementary cumulative distribution function (basically 1-p).\n\n# Select rainfall events lower or equal than a specific amount\namount = 5\n\n# Use actual data to estimate the probability\np = stats.lognorm.cdf(amount, *fitted_pdf.params)\nprint(f'The probability of having a rainfall event <= {amount} mm is {p:.2f}')\nprint(f'The probability of having a rainfall event > {amount} mm is {1-p:.2f}')\n\nThe probability of having a rainfall event <= 5 mm is 0.75\nThe probability of having a rainfall event > 5 mm is 0.25\n\n\n\n\n\n\n\n\nNote\n\n\n\nTo determine the probability using the observations, rather than the fitted theoretical distribution, we can simply use the number of favorable cases over the total number of possibilities, like this:\nidx_threshold = data <= amount\np = np.sum(idx_threshold) / np.sum(idx_rained)\n\n\n\n# Cumulative distributions\nplt.figure(figsize=(6,4))\nplt.ecdf(data, label=\"Empirical CDF\")\nplt.hist(data, bins=200, density=True, histtype=\"step\",\n cumulative=True, label=\"Cumulative histogram\")\nplt.plot(x, stats.lognorm.cdf(x, *fitted_pdf.params), label='Theoretical CDF')\n\nplt.plot([amount, amount, 0],[0, p, p], linestyle='--', color='k')\nplt.title('Daily Precipitation Greeley County, KS 1980-2020')\nplt.xlabel('Precipitation (mm)')\nplt.ylabel('Cumulative density')\nplt.xlim([0, 20])\nplt.legend()\nplt.show()\n\n\n\n\nIn this tutorial we learned that: - typical daily rainfall events in places like western Kansas tend to be very small, in the order of 2 mm. We found this by inspecting a histogram and computing the median of all days with measurable rainfall.\n\nthe probability of having rainfall events larger than 5 mm is only 25%. We found this by using a cumulative density function. If we consider that rainfall interception by plant canopies and crop residue can range from 1 to several millimeters, daily rainfall events will be ineffective reaching the soil surface in regions with this type of daily rainfall distributions, where most of the soil water recharge will depend on larger rainfall events." + }, + { + "objectID": "exercises/distribution_daily_precipitation.html#references", + "href": "exercises/distribution_daily_precipitation.html#references", + "title": "39  Distribution daily precipitation", + "section": "References", + "text": "References\nClark, O. R. (1940). Interception of rainfall by prairie grasses, weeds, and certain crop plants. Ecological monographs, 10(2), 243-277.\nDunkerley, D. (2000). Measuring interception loss and canopy storage in dryland vegetation: a brief review and evaluation of available research strategies. Hydrological Processes, 14(4), 669-678.\nKang, Y., Wang, Q. G., Liu, H. J., & Liu, P. S. (2004). Winter wheat canopy-interception with its influence factors under sprinkler irrigation. In 2004 ASAE Annual Meeting (p. 1). American Society of Agricultural and Biological Engineers." + }, + { + "objectID": "exercises/high_resolution_rainfall_events.html#references", + "href": "exercises/high_resolution_rainfall_events.html#references", + "title": "40  High-resolution rainfall events", + "section": "References", + "text": "References\nDunkerley, D. (2015). Intra‐event intermittency of rainfall: An analysis of the metrics of rain and no‐rain periods. Hydrological Processes, 29(15), 3294-3305.\nDyer, D. W., Patrignani, A., & Bremer, D. (2022). Measuring turfgrass canopy interception and throughfall using co-located pluviometers. Plos one, 17(9), e0271236. https://doi.org/10.1371/journal.pone.0271236" + }, + { + "objectID": "exercises/first_and_last_frost.html#find-date-of-last-frost", + "href": "exercises/first_and_last_frost.html#find-date-of-last-frost", + "title": "41  First and last frost", + "section": "Find date of last frost", + "text": "Find date of last frost\n\n# Define variables for last frost\nlf_start_doy = 1\nlf_end_doy = 183 # Around July 15 (to be on the safe side)\nfreeze_temp = 0 # Celsius\n\n\n# Get unique years\nunique_years = df['year'].unique()\nlf_doy = []\n\nfor year in unique_years:\n idx_year = df['year'] == year\n idx_lf_period = (df['doy'] > lf_start_doy) & (df['doy'] <= lf_end_doy)\n idx_frost = df['tmmn'] < freeze_temp\n idx = idx_year & idx_lf_period & idx_frost\n \n # Select all DOY for current year that meet all conditions.\n # Sort in ASCENDING order. The last value was the last freezing DOY\n all_doy_current_year = df.loc[idx, 'doy'].sort_values()\n lf_doy.append(all_doy_current_year.iloc[-1])" + }, + { + "objectID": "exercises/first_and_last_frost.html#find-date-of-first-frost", + "href": "exercises/first_and_last_frost.html#find-date-of-first-frost", + "title": "41  First and last frost", + "section": "Find date of first frost", + "text": "Find date of first frost\n\n# Define variables for first frost\nff_start_doy = 183 # Around July 15 (to be on the safe side)\nff_end_doy = 365\n\n\n# Get unique years\nff_doy = []\n\nfor year in unique_years:\n idx_year = df['year'] == year\n idx_ff_period = (df['doy'] > ff_start_doy) & (df['doy'] <= ff_end_doy)\n idx_frost = df['tmmn'] < freeze_temp\n idx = idx_year & idx_ff_period & idx_frost\n \n # Select all DOY for current year that meet all conditions.\n # Sort in DESCENDING order. The last value was the last freezing DOY\n all_doy_current_year = df.loc[idx, 'doy'].sort_values(ascending=False)\n ff_doy.append(all_doy_current_year.iloc[-1])" + }, + { + "objectID": "exercises/first_and_last_frost.html#find-median-date-for-first-and-last-frost", + "href": "exercises/first_and_last_frost.html#find-median-date-for-first-and-last-frost", + "title": "41  First and last frost", + "section": "Find median date for first and last frost", + "text": "Find median date for first and last frost\n\n# Create dataframe with the first and last frost for each year\n# The easiest is to create a dictionary with the variables we already have\n\ndf_frost = pd.DataFrame({'year':unique_years,\n 'first_frost_doy':ff_doy,\n 'last_frost_doy':lf_doy})\ndf_frost.head(3)\n\n\n\n\n\n\n\n\nyear\nfirst_frost_doy\nlast_frost_doy\n\n\n\n\n0\n1980\n299\n104\n\n\n1\n1981\n296\n79\n\n\n2\n1982\n294\n100\n\n\n\n\n\n\n\n\n# Print median days of the year\ndf_frost[['first_frost_doy','last_frost_doy']].median()\n\nfirst_frost_doy 298.0\nlast_frost_doy 96.0\ndtype: float64\n\n\n\n# Compute median DOY and calculate date for first frost\nfirst_frost_median_doy = df_frost['first_frost_doy'].median()\nfirst_frost_median_date = pd.to_datetime('2000-01-01') + pd.Timedelta(first_frost_median_doy, 'days')\nprint(f\"Median date first frost: {first_frost_median_date.strftime('%d-%B')}\")\n\nfirst_frost_earliest_doy = df_frost['first_frost_doy'].min() # Min value for earliest first frost\nfirst_frost_earliest_date = pd.to_datetime('2000-01-01') + pd.Timedelta(first_frost_earliest_doy, 'days')\nprint(f\"Earliest date first frost on record: {first_frost_earliest_date.strftime('%d-%B')}\")\n\n# Compute median DOY and calculate date for first frost\nlast_frost_median_doy = df_frost['last_frost_doy'].median()\nlast_frost_median_date = pd.to_datetime('2000-01-01') + pd.Timedelta(last_frost_median_doy, 'days')\nprint(f\"Median date last frost: {last_frost_median_date.strftime('%d-%B')}\")\n\nlast_frost_latest_doy = df_frost['last_frost_doy'].max() # Max value for latest last frost\nlast_frost_latest_date = pd.to_datetime('2000-01-01') + pd.Timedelta(last_frost_latest_doy, 'days')\nprint(f\"Latest date last frost on record: {last_frost_latest_date.strftime('%d-%B')}\")\n\nMedian date first frost: 25-October\nEarliest date first frost on record: 23-September\nMedian date last frost: 06-April\nLatest date last frost on record: 30-April" + }, + { + "objectID": "exercises/first_and_last_frost.html#compute-frost-free-period", + "href": "exercises/first_and_last_frost.html#compute-frost-free-period", + "title": "41  First and last frost", + "section": "Compute frost-free period", + "text": "Compute frost-free period\n\n# Period without any risk of frost\nfrost_free = first_frost_earliest_doy - last_frost_latest_doy\nprint(f'Frost-free period: {frost_free} days')\n\nFrost-free period: 146 days" + }, + { + "objectID": "exercises/first_and_last_frost.html#probability-density-functions", + "href": "exercises/first_and_last_frost.html#probability-density-functions", + "title": "41  First and last frost", + "section": "Probability density functions", + "text": "Probability density functions\nLet’s first examine if a normal distribution fits the observations using probability plots, which compare the distribution of our data against the quantiles of a specified theoretical distribution (normal distribution in this case, similar to qq-plots). If the agreement is good, then this provides some support for using the selected distribution.\n\n# Check distribution of data\nplt.figure(figsize=(8,3))\nplt.subplot(1,2,1)\nstats.probplot(df_frost['first_frost_doy'], dist=\"norm\", rvalue=True, plot=plt)\nplt.title('First frost')\nplt.subplot(1,2,2)\nstats.probplot(df_frost['last_frost_doy'], dist=\"norm\", rvalue=True, plot=plt)\nplt.title('Last frost')\nplt.ylabel('')\nplt.show()\n\n\n\n\n\n# Fit normal distributions\nfitted_pdf_ff = stats.fit(stats.norm, df_frost['first_frost_doy'], bounds=((180,365),(1,25)))\nprint(fitted_pdf_ff.params)\n\nfitted_pdf_lf = stats.fit(stats.norm, df_frost['last_frost_doy'], bounds=((1,180),(1,25)))\nprint(fitted_pdf_lf.params)\n\nFitParams(loc=296.12195397938365, scale=13.816206118531046)\nFitParams(loc=95.39034225034837, scale=11.17897667307393)\n\n\n\n# Create vector for the normal pdf of first frost\nx_ff = np.linspace(df_frost['first_frost_doy'].min(), \n df_frost['first_frost_doy'].max(), \n num=1000)\n\n# Create vector for the normal pdf of last frost\nx_lf = np.linspace(df_frost['last_frost_doy'].min(), \n df_frost['last_frost_doy'].max(), \n num=1000)\n\n\n# Figure of free-frost period\nplt.figure(figsize=(6,3))\nplt.title('Johnson County, KS')\n\n# Add histograms for first and last frost\nplt.hist(df_frost['first_frost_doy'], bins='scott', density=True,\n label='Median first frost', facecolor=(0,0.5,1,0.25), edgecolor='navy')\nplt.hist(df_frost['last_frost_doy'], bins='scott', density=True,\n label='Median last frost', facecolor=(1,0.2,0,0.25), edgecolor='tomato')\n\n# Add median lines\nplt.axvline(last_frost_median_doy, linestyle='--', color='tomato')\nplt.axvline(first_frost_median_doy, linestyle='--', color='navy')\n\n# Overlay fitted distributions to each histogram\nplt.plot(x_ff, stats.norm.pdf(x_ff, *fitted_pdf_ff.params),\n color='navy', label='First frost pdf')\nplt.plot(x_lf, stats.norm.pdf(x_lf, *fitted_pdf_lf.params),\n color='tomato', label='Last frost pdf')\n\n# Add filled area to show the frost-free period\nplt.fill_betweenx(np.linspace(0,0.05), last_frost_latest_doy, first_frost_earliest_doy,\n facecolor=(0, 0.5, 0.5, 0.2), edgecolor='k')\n\n# Add some annotations\nplt.text(145, 0.04, \"Frost-free period\", size=14)\nplt.text(165, 0.035, f\"{frost_free} days\", size=14)\n\nplt.ylim([0, 0.05])\nplt.xlabel('Day of the year')\nplt.ylabel('Density')\nplt.minorticks_on()\nplt.legend()\nplt.show()\n\n\n\n\n\n# Cumulative distributions\n# Create vector from 0 to x_max to plot the lognorm pdf\nx = np.linspace(df_frost['first_frost_doy'].min(),\n df_frost['first_frost_doy'].max(),\n num=1000)\n\nplt.figure(figsize=(10,4))\nplt.suptitle('Johnson County, KS 1980-2020')\n\n# First frost\nplt.subplot(1,2,1)\nplt.ecdf(df_frost['first_frost_doy'], label=\"Empirical CDF\")\nplt.plot(x_ff, stats.norm.cdf(x_ff, *fitted_pdf_ff.params), label='Theoretical CDF')\nplt.title('First frost')\nplt.xlabel('Day of the year')\nplt.ylabel('Cumulative density')\nplt.legend()\n\n# Last frost (note the use of the complementary CDF)\nplt.subplot(1,2,2)\nplt.ecdf(df_frost['last_frost_doy'], complementary=True, label=\"Empirical CDF\")\nplt.plot(x_lf, 1-stats.norm.cdf(x_lf, *fitted_pdf_lf.params), label='Theoretical CDF')\nplt.title('Last frost')\nplt.xlabel('Day of the year')\nplt.ylabel('Cumulative density')\nplt.legend()\n\nplt.show()\n\n\n\n\n\n# Determine the probability of a first frost occurying on or before:\ndoy = 245 # September 1\nstats.norm.cdf(doy, *fitted_pdf_ff.params)\n\n# As expected, if you change the DOY for a value closer to July 1,\n# the chances of frost in the north hemisphere are going to decrease to nearly zero.\n\n0.00010773852588002489\n\n\n\n# Determine the probability of a frost occurying on or after:\ndoy = 122 # May 1\nstats.norm.sf(doy, *fitted_pdf_lf.params)\n\n# As expected, if you change the DOY for a value closer to January 1,\n# the chances of frost in the north hemisphere are going to increase to 1 (or 100%).\n\n0.008648561213750895\n\n\n\n\n\n\n\n\nNote\n\n\n\nWhy did we use the complementary of the CDF for determining the probability of last frost? We used the complementary CDF (or sometimes known as the survival function, hence the syntax .sf(), because in most cases we are interested in knowing the probability of a frost “on or after” a given date. For instance, a farmer that is risk averse will prefer to plant corn when the chances of a late frost that can kill the entire crop overnight is very low." + }, + { + "objectID": "exercises/central_dogma.html#transcription", + "href": "exercises/central_dogma.html#transcription", + "title": "42  Central dogma", + "section": "Transcription", + "text": "Transcription\nGiven a sequence of DNA bases we need to find the complementary strand. The catch here is that we also need to account for the fact that the base thymine is replaced by the base uracil in RNA.\nTo check for potential typos in the sequence of DNA or to prevent that the user feeds a sequence of mRNA instead of DNA to the transcription function, we will use the raise statement, which will automatically stop and exit the for loop and throw a custom error message if the code finds a base a base other than A,T,C, or G. The location of the raise statement is crucial since we only want to trigger this action if a certain condition is met (i.e. we find an unknown base). So, we will place the raise statement inside the if statement within the for loop. We will also return the location in the sequence of the unknown base using the find() method.\nThe error catching method described above is simple and practical for small applications, but it has some limitations. For instance, we cannot identify whether there are more than one unknwon bases and we cannot let the user know the location of all these bases. Nonetheless, this is a good starting point.\n\ndef transcription(DNA):\n '''\n Function that converts a sequence of DNA bases into messenger RNA\n Input: string of DNA\n Author: Andres Patrignani\n Date: 3-Feb-2020\n '''\n # Translation table\n transcription_table = DNA.maketrans('ATCG','UAGC')\n #print(transcription_table) {65: 85, 84: 65, 67: 71, 71: 67}\n \n # Translate using table\n mRNA = DNA.translate(transcription_table)\n return mRNA" + }, + { + "objectID": "exercises/central_dogma.html#translation", + "href": "exercises/central_dogma.html#translation", + "title": "42  Central dogma", + "section": "Translation", + "text": "Translation\nThe logic of the translation function will be similar to our previous example. The only catch is that we need to keep track of the different polypeptides and the start and stop signals in the mRNA. These signals dictate the sequence of aminoacids for each polypeptide. Here are some steps of the logic:\n\nScan the mRNA in steps of three bases\nTrigger a new polypeptide only when we find the starting ‘AUG’ codon\nAfter that we know the ribosome is inside the mRNA that encodes aminoacids\nThe end of the polypeptide occurs when the ribosome finds any of the stop codons: ‘UAA’, ‘UAG’, ‘UGA’\n\n\n# Translation function\n\ndef translation(mRNA):\n '''\n Function that decodes a sequence of mRNA into a chain of aminoacids\n Input: string of mRNA\n Author: Andres Patrignani\n Date: 27-Dec-2019\n '''\n \n # Initialize variables\n polypeptides = dict() # More convenient and human-readable than creating a list of lists\n start = False # Ribosome outside region of mRNA that encodes aminoacids\n polypeptide_counter = 0 # A counter to name our polypetides\n \n for i in range(0,len(mRNA)-2,3):\n codon = mRNA[i:i+3] # Add 3 to avoid overlapping the bases between iterations.\n aminoacid_idx = lookup.codon == codon # Match current codon with all codons in lookup table\n aminoacid = lookup.aminoacid[aminoacid_idx].values[0]\n \n # Logic to find in which polypeptide the Ribosome is in\n if codon == 'AUG':\n start = True\n polypeptide_counter += 1 \n polypeptide_name = 'P' + str(polypeptide_counter)\n polypeptides[polypeptide_name] = []\n \n elif codon == 'UAA' or codon == 'UAG' or codon == 'UGA':\n start = False\n \n # If the Ribosme found a starting codon (Methionine)\n if start:\n polypeptides[polypeptide_name].append(aminoacid)\n \n return polypeptides\n \n\nIn the traslation function we could have used if aminoacid == 'Methionine': for the first logical statement and elif aminoacid == 'Stop': for the second logical statement. I decided to use the codons rather than the aminoacids to closely match the mechanics of the Ribosome, but the statements are equivalent in terms of the outputs that the function generates.\n\nQ: What happens if you indent four additional spaces the line: return polypeptide in the translation function? You will need to modify, save, and call the function to see the answer to this question.\n\n\nDNA = 'TACTCGTCACAGGTTACCCCAAACATTTACTGCGACGTATAAACTTACTGCACAAATGTGACT'\nmRNA = transcription(DNA)\nprint(mRNA)\npolypeptides = translation(mRNA)\npprint.pprint(polypeptides)\n\nAUGAGCAGUGUCCAAUGGGGUUUGUAAAUGACGCUGCAUAUUUGAAUGACGUGUUUACACUGA\n{'P1': ['Methionine',\n 'Serine',\n 'Serine',\n 'Valine',\n 'Glutamine',\n 'Tryptophan',\n 'Glycine',\n 'Leucine'],\n 'P2': ['Methionine', 'Threonine', 'Leucine', 'Histidine', 'Isoleucine'],\n 'P3': ['Methionine', 'Threonine', 'Cysteine', 'Leucine', 'Histidine']}" + }, + { + "objectID": "exercises/error_metrics.html#residuals", + "href": "exercises/error_metrics.html#residuals", + "title": "43  Error metrics", + "section": "Residuals", + "text": "Residuals\nThe residuals are the differences between the observed values and the predicted values. Residuals are a diagnostic measure to understand whether the model or a sensor has systematically overestimated or underestimated the benchmark data. Analyzing the pattern of residuals can reveal biases in the model or indicate whether certain assumptions of the model are not being met.\n\nresiduals = y_pred - y_obs\nprint(residuals)\n\n0 -2.5\n1 -3.7\n2 3.5\n3 5.4\n4 0.2\n ... \n93 -2.1\n94 1.9\n95 1.7\n96 7.1\n97 5.1\nLength: 98, dtype: float64\n\n\n\n# Visually inspect residuals\nplt.figure(figsize=(5,4))\nplt.scatter(y_obs, residuals, facecolor=(1,0.2,0.2,0.5), edgecolor='k')\nplt.axhline(0, linestyle='--', color='k')\nplt.xlabel('Observed (%)')\nplt.ylabel('Residuals (%)')\nplt.show()\n\n\n\n\nInspection of the residuals revealed that at low soil moisture levels the sensor tends to underestimate soil moisture and that at high soil moisture levels the sensor tends to overestimate soil moisture." + }, + { + "objectID": "exercises/error_metrics.html#mean-bias-error-mbe", + "href": "exercises/error_metrics.html#mean-bias-error-mbe", + "title": "43  Error metrics", + "section": "Mean bias error (MBE)", + "text": "Mean bias error (MBE)\nThe MBE determines the average bias, showing whether the model, sensor, or measurement consistently overestimates or underestimates compared to the benchmark. In the case of the MBE, positive values mean over-prediction and negative values under-prediction. Although this would depend on the order of the subtraction when computing the residuals.\nA bias equal to zero can be a consequence of small errors or very large errors balanced by opposite sign. It is always recommended to include other error metrics in addition to the mean bias error.\n\nmbe = np.nanmean(residuals)\nprint(mbe)\n\n0.0836734693877551" + }, + { + "objectID": "exercises/error_metrics.html#sum-of-residuals-sres", + "href": "exercises/error_metrics.html#sum-of-residuals-sres", + "title": "43  Error metrics", + "section": "Sum of residuals (SRES)", + "text": "Sum of residuals (SRES)\n\n# Sum of residuals\nsres = np.nansum(residuals)\nprint(sres)\n\n8.200000000000001" + }, + { + "objectID": "exercises/error_metrics.html#sum-of-the-absolute-of-residuals-sares", + "href": "exercises/error_metrics.html#sum-of-the-absolute-of-residuals-sares", + "title": "43  Error metrics", + "section": "Sum of the absolute of residuals (SARES)", + "text": "Sum of the absolute of residuals (SARES)\n\n# Sum of absolute residuals\nsares = np.nansum(np.abs(residuals))\nprint(sares)\n\n391.6" + }, + { + "objectID": "exercises/error_metrics.html#sum-of-squared-errors-or-residuals", + "href": "exercises/error_metrics.html#sum-of-squared-errors-or-residuals", + "title": "43  Error metrics", + "section": "Sum of squared errors (or residuals)", + "text": "Sum of squared errors (or residuals)\n\n# Sum of squared errors\nsse = np.nansum(residuals**2)\nprint(sse)\n\n2459.6400000000003" + }, + { + "objectID": "exercises/error_metrics.html#mean-squared-error-mse", + "href": "exercises/error_metrics.html#mean-squared-error-mse", + "title": "43  Error metrics", + "section": "Mean squared error (MSE)", + "text": "Mean squared error (MSE)\n\n# Mean squared error\nmse = np.nanmean(residuals**2)\nprint(mse)\n\n25.09836734693878" + }, + { + "objectID": "exercises/error_metrics.html#root-mean-squared-error-rmse", + "href": "exercises/error_metrics.html#root-mean-squared-error-rmse", + "title": "43  Error metrics", + "section": "Root mean squared error (RMSE)", + "text": "Root mean squared error (RMSE)\nThe RMSE is one of the most popular error metrics in modeling studies and quantifies the square root of the average of squared differences between the predicted or measured values and the benchmark. It emphasizes larger errors, making it useful for understanding substantial discrepancies, but this feature also makes it very sensitive to outliers.\nWhen comparing two estimates where none of them represents the ground truth it is better to name this error metric the “Root Mean Squared Difference” to emphasize that is the difference between two estimates. The word “error” is typically reserved to represent deviations against a gold standard or ground-truth value.\n\n# Root mean squared error\nrmse = np.sqrt(np.nanmean(residuals**2))\nprint(rmse)\n\n5.009827077548564" + }, + { + "objectID": "exercises/error_metrics.html#relative-root-mean-squared-error-rrmse", + "href": "exercises/error_metrics.html#relative-root-mean-squared-error-rrmse", + "title": "43  Error metrics", + "section": "Relative root mean squared error (RRMSE)", + "text": "Relative root mean squared error (RRMSE)\nThe RRMSE is more meaningful than the RMSE when comparing errors from datasets with different units or ranges. Sometimes the RRMSE is computed by dividing the RMSE over the range of the observed values rather than the average of the observed values.\n\n# Realtive root mean squared error\nrrmse = np.sqrt(np.nanmean(residuals**2)) / np.nanmean(y_obs)\nprint(rrmse)\n\n0.20987605420414623" + }, + { + "objectID": "exercises/error_metrics.html#mean-absolute-error-mae", + "href": "exercises/error_metrics.html#mean-absolute-error-mae", + "title": "43  Error metrics", + "section": "Mean absolute error (MAE)", + "text": "Mean absolute error (MAE)\nThe MAE measures the average magnitude of the absolute errors between the predictions or measurements and the benchmark, treating all deviations equally without regard to direction. As a result, the MAE is a more robust error metric against outliers compared to the RMSE.\n\nmae = np.nanmean(np.abs(residuals))\nprint(mae)\n\n3.995918367346939" + }, + { + "objectID": "exercises/error_metrics.html#median-absolute-error", + "href": "exercises/error_metrics.html#median-absolute-error", + "title": "43  Error metrics", + "section": "Median absolute error", + "text": "Median absolute error\nThe MedAE calculates the median of the absolute differences between the benchmark values and the predictions, providing a measure of the typical error size. The use of the median gives the MedAE an advantage over the mean since it is less sensitive to outliers.\n\n# Median absolute error\nmedae = np.nanmedian(np.abs(residuals))\nprint(medae)\n\n3.1999999999999975" + }, + { + "objectID": "exercises/error_metrics.html#willmott-index-of-agreement-d", + "href": "exercises/error_metrics.html#willmott-index-of-agreement-d", + "title": "43  Error metrics", + "section": "Willmott index of agreement (D)", + "text": "Willmott index of agreement (D)\nThis index offers a normalized measure, ranging from 0 (no agreement) to 1 (perfect agreement, y_obs=y_pred, and consequently SSE=0), evaluating the relative error between the predicted/measured values and the benchmark. It is particularly useful for addressing the limitations of other statistical measures.\n\nabs_diff_pred = np.abs(y_pred - np.nanmean(y_obs))\nabs_diff_obs = np.abs(y_obs - np.nanmean(y_obs))\n\nwillmott = 1 - np.nansum(residuals**2) / np.nansum((abs_diff_pred + abs_diff_obs)**2)\nprint(willmott)\n\n0.9717700524449996" + }, + { + "objectID": "exercises/error_metrics.html#nash-sutcliffe-efficiency", + "href": "exercises/error_metrics.html#nash-sutcliffe-efficiency", + "title": "43  Error metrics", + "section": "Nash-Sutcliffe Efficiency", + "text": "Nash-Sutcliffe Efficiency\nThe NSE assesses the predictive power of models or measurements by comparing the variance of the residuals to the variance of the observed data. An NSE of 1 suggests an excellent match, while values below 0 imply that the average of the observed data is a better predictor than the model or measurement under scrutiny.\n\n# Nash-Sutcliffe Efficiency\n\nnumerator = np.sum(residuals**2)\ndenominator = np.sum((y_obs - np.mean(y_obs))**2)\nnse = 1 - numerator/denominator\nprint(nse)\n\n0.8653258914122538\n\n\n\n\n\n\n\n\nCaution\n\n\n\nThe Nash-Sutcliffe Efficiency metric is widely used to compare predicted and observed time series in hydrology (e.g., streamflow)." + }, + { + "objectID": "exercises/error_metrics.html#references", + "href": "exercises/error_metrics.html#references", + "title": "43  Error metrics", + "section": "References", + "text": "References\nWillmott, C.J., Robeson, S.M. and Matsuura, K., 2012. A refined index of model performance. International Journal of Climatology, 32(13), pp.2088-2094.\nWillmott, C.J. and Matsuura, K., 2005. Advantages of the mean absolute error (MAE) over the root mean square error (RMSE) in assessing average model performance. Climate research, 30(1), pp.79-82.\nWillmott, C.J., 1981. On the validation of models. Physical geography, 2(2), pp.184-194." + }, + { + "objectID": "exercises/request_web_data.html#example-1-national-weather-service", + "href": "exercises/request_web_data.html#example-1-national-weather-service", + "title": "44  Request web data", + "section": "Example 1: National Weather Service", + "text": "Example 1: National Weather Service\nThe National Weather Service has a free Application Programming Interface (API) that we can use to request forecast and alert weather conditions. The API has several end points, in this example we will use the end point to retrieve weather forecasts based on geographic coordinates.\n\n# Define location latitude and longitude\nlatitude = 37\nlongitude = -97\n\n# Build API url\nurl = f\"https://api.weather.gov/points/{latitude},{longitude}\"\n\n# Make the request\nresult = requests.get(url)\n\n\n# Get the request output in Javascript Object Notation (JSON) format\noutput = result.json()\noutput['properties']['forecast']\n\n'https://api.weather.gov/gridpoints/ICT/73,3/forecast'\n\n\n\n# Request forecast for next seven days in 12-hour periods\nforecast_url = output['properties']['forecast']\nforecast = requests.get(forecast_url).json()\n\n\n# Create dictionary to append variables of interest\ndata = {'start_time':[], 'temperature':[], 'sky_conditions':[]}\n\nfor day in forecast['properties']['periods']:\n data['start_time'].append(day['startTime'])\n data['temperature'].append(day['temperature'])\n data['sky_conditions'].append(day['shortForecast'])\n \n# Convert lists to Pandas Dataframe\ndf = pd.DataFrame(data)\ndf['start_time'] = pd.to_datetime(df['start_time'])\ndf.head(3)\n\n\n\n\n\n\n\n\nstart_time\ntemperature\nsky_conditions\n\n\n\n\n0\n2024-02-19 10:00:00-06:00\n58\nSunny\n\n\n1\n2024-02-19 18:00:00-06:00\n34\nClear\n\n\n2\n2024-02-20 06:00:00-06:00\n71\nSunny\n\n\n\n\n\n\n\n\n# Create a figure of the forecast\nplt.figure(figsize=(7,4))\nplt.title(f'Forecast for {latitude}, {longitude}')\nplt.plot(df['start_time'], df['temperature'], '-o')\nplt.ylabel(f'Air temperature ({chr(176)}F)')\nplt.xticks(rotation=25)\nplt.ylim([df['temperature'].min()-5, df['temperature'].max()+5])\nfor k,row in df.iterrows():\n plt.text(row['start_time'], row['temperature']+1, row['sky_conditions'], fontsize=8)\nplt.show()" + }, + { + "objectID": "exercises/request_web_data.html#example-2-kansas-mesonet", + "href": "exercises/request_web_data.html#example-2-kansas-mesonet", + "title": "44  Request web data", + "section": "Example 2: Kansas Mesonet", + "text": "Example 2: Kansas Mesonet\nIn this example we will learn to download data from the Kansas mesonet. Data can be accessed through a URL (Uniform Resource Locator), which is also known as a web address. In this URL we are going to pass some parameters to specify the location, date, and interval of the data. For services that output their data in comma-separated values we can use the Pandas library.\nKansas mesonet REST API: http://mesonet.k-state.edu/rest/\n\n# Define function to request data\ndef get_ks_mesonet(station, start_date, end_date, variables, interval='day'):\n \"\"\"\n Function to retrieve air temperature for a specific station \n and period from the Kansas Mesonet\n \n Parameters\n ----------\n station : string\n Station name\n start_date : string\n yyyy-mm-dd format\n end_date : string\n yyyy-mm-dd fomat\n variables : list\n Weather variables to download\n interval : string\n One of the following: '5min', 'hour', or 'day'\n \n Returns\n -------\n df : Dataframe\n Table with data for specified station and period.\n \n \"\"\"\n \n # Define date format as required by the API\n fmt = '%Y%m%d%H%M%S'\n start_date = pd.to_datetime(start_date).strftime(fmt)\n end_date = pd.to_datetime(end_date).strftime(fmt)\n \n # Concatenate variables using comma\n variables = ','.join(variables)\n \n # Create URL\n url = f\"http://mesonet.k-state.edu/rest/stationdata/?stn={station}&int={interval}&t_start={start_date}&t_end={end_date}&vars={variables}\"\n \n # A URL cannot have spaces, so we replace them with %20\n url = url.replace(\" \", \"%20\")\n \n # Crete Dataframe and replace missing values by NaN\n df = pd.read_csv(url, na_values='M') # Request data and replace missing values represented by \"M\" for NaN values.\n \n return df\n\n\n# Use function to request data\nstation = 'Manhattan'\nstart_date = '2024-01-01'\nend_date = '2024-01-15'\nvariables = ['TEMP2MAVG','PRECIP']\n\ndf_ks_mesonet = get_ks_mesonet(station, start_date, end_date, variables)\ndf_ks_mesonet.head()\n\n\n\n\n\n\n\n\nTIMESTAMP\nSTATION\nTEMP2MAVG\nPRECIP\n\n\n\n\n0\n2024-01-01 00:00:00\nManhattan\n-1.40\n0.0\n\n\n1\n2024-01-02 00:00:00\nManhattan\n-2.15\n0.0\n\n\n2\n2024-01-03 00:00:00\nManhattan\n-1.09\n0.0\n\n\n3\n2024-01-04 00:00:00\nManhattan\n-1.20\n0.0\n\n\n4\n2024-01-05 00:00:00\nManhattan\n-0.38\n0.0" + }, + { + "objectID": "exercises/request_web_data.html#example-3-u.s.-geological-survey-streamflow-data", + "href": "exercises/request_web_data.html#example-3-u.s.-geological-survey-streamflow-data", + "title": "44  Request web data", + "section": "Example 3: U.S. Geological Survey streamflow data", + "text": "Example 3: U.S. Geological Survey streamflow data\n\ndef get_usgs(station, start_date, end_date=None):\n \"\"\"\n Function to retreive 15-minute streamflow data from the U.S. Geological Survey.\n \n Parameters\n ----------\n station : string\n 8-digit number of the USGS streamflow gauge of interest\n start_date : string\n Start UTC date in yyyy-mm-dd HH:MM:SS format\n end_date : string (optional)\n End UTC date in yyyy-mm-dd HH:MM:SS format.\n If not provided, the dataset will span from start_date until present time\n\n \n Returns\n -------\n df : Dataframe\n Table with 15-minute streamflow data.\n 'A' stands for active\n 'P' stands for \"Provisional\" data subject to revision.\n 'datetime' is reported in local time (including standard and daylight time)\n 'discharge' is in ft^3/s\n 'height' is in ft\n \n API docs: https://waterservices.usgs.gov/docs/instantaneous-values/instantaneous-values-details/\n \"\"\"\n\n # Check that station identifier has 8 digits\n if len(station) != 8:\n raise ValueError(\"Station must be an an 8-digit code\")\n \n # Convert strings to UTC time using ISO format\n start_date = pd.to_datetime(start_date).isoformat() + 'Z'\n if end_date is not None:\n end_date = pd.to_datetime(end_date).isoformat() + 'Z'\n \n # Check that start date is smaller than end date\n if start_date > end_date:\n raise ValueError(\"start_date cannot be greater than end_date\")\n\n \n # Build URL and define parameter values for request\n url = 'https://waterservices.usgs.gov/nwis/iv/'\n params = {'format': 'rdb', \n 'sites': '06879650', \n 'startDT': start_date,\n 'endDT':end_date,\n 'parameterCd': '00060,00065', # discharge and height\n 'siteStatus':'active'}\n\n # Request data\n response = requests.get(url, params=params)\n s = response.content\n \n df = pd.read_csv(io.StringIO(s.decode('utf-8')), sep='\\t', comment='#')\n if df.shape[0] == 0:\n raise IndexError(\"DataFrame is empty\")\n \n df.drop([0], inplace=True)\n df.reset_index(inplace=True, drop=True)\n df.rename(columns={\"56608_00060\": \"discharge_ft3_s\",\n \"56608_00060_cd\": \"status_discharge\",\n \"56607_00065\": \"height_ft\",\n \"56607_00065_cd\": \"status_height\"}, inplace=True)\n\n # Convert dates to datetime format\n df['datetime'] = pd.to_datetime(df['datetime'])\n\n # Convert discharge and height to float type\n df['discharge_ft3_s'] = df['discharge_ft3_s'].astype(float)\n df['height_ft'] = df['height_ft'].astype(float)\n \n return df\n \n\n\n# Download data for the Kings Creek watershed within\n# the Konza Prairie Biological Station near Manhattan, KS\nstation = '06879650' # Kings Creek watershed\nstart_date = '2023-04-15'\nend_date = '2023-07-15'\ndf_usgs = get_usgs(station, start_date, end_date)\n\n\n# Display downloaded data\ndf_usgs.head(3)\n\n\n\n\n\n\n\n\nagency_cd\nsite_no\ndatetime\ntz_cd\ndischarge_ft3_s\nstatus_discharge\nheight_ft\nstatus_height\n\n\n\n\n0\nUSGS\n06879650\n2023-04-14 18:00:00\nCDT\n0.0\nA\n2.5\nA\n\n\n1\nUSGS\n06879650\n2023-04-14 18:15:00\nCDT\n0.0\nA\n2.5\nA\n\n\n2\nUSGS\n06879650\n2023-04-14 18:30:00\nCDT\n0.0\nA\n2.5\nA\n\n\n\n\n\n\n\n\n# Convert discharge from ft^3/s to cubic m^3/s\ndf_usgs['discharge_m3_per_s'] = df_usgs['discharge_ft3_s']*0.0283168 \n\n# Convert height from ft to m\ndf_usgs['height_m'] = df_usgs['height_ft']/0.3048\n\n# Standardize all timestamps to UTC\nidx_cdt = df_usgs['tz_cd'] == 'CDT'\ndf_usgs.loc[idx_cdt,'datetime'] = df_usgs.loc[idx_cdt,'datetime'] + pd.Timedelta('5H')\n\nidx_cst = df_usgs['tz_cd'] == 'CST'\ndf_usgs.loc[idx_cst,'datetime'] = df_usgs.loc[idx_cst,'datetime'] + pd.Timedelta('6H')\n\n# Replace label from CST/CDT to UTC\ndf_usgs['tz_cd'] = 'UTC'\n\n# Check our changes\ndf_usgs.head(3)\n\n# Save to drive\n#filename = f\"{station}_streamflow.csv\"\n#df.to_csv(filename, index=False)\n\n\n\n\n\n\n\n\nagency_cd\nsite_no\ndatetime\ntz_cd\ndischarge_ft3_s\nstatus_discharge\nheight_ft\nstatus_height\ndischarge_m3_per_s\nheight_m\n\n\n\n\n0\nUSGS\n06879650\n2023-04-14 23:00:00\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n1\nUSGS\n06879650\n2023-04-14 23:15:00\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n2\nUSGS\n06879650\n2023-04-14 23:30:00\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n\n\n\n\n\n\n# Set 'datetime' column as timeindex\ndf_usgs.set_index('datetime', inplace=True)\ndf_usgs.head(3)\n\n\n\n\n\n\n\n\nagency_cd\nsite_no\ntz_cd\ndischarge_ft3_s\nstatus_discharge\nheight_ft\nstatus_height\ndischarge_m3_per_s\nheight_m\n\n\ndatetime\n\n\n\n\n\n\n\n\n\n\n\n\n\n2023-04-14 23:00:00\nUSGS\n06879650\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n2023-04-14 23:15:00\nUSGS\n06879650\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n2023-04-14 23:30:00\nUSGS\n06879650\nUTC\n0.0\nA\n2.5\nA\n0.0\n8.2021\n\n\n\n\n\n\n\n\n# Aggregate data hourly\ndf_usgs_daily = df_usgs.resample('1D').agg({'agency_cd':np.unique,\n 'site_no':np.unique,\n 'tz_cd':np.unique,\n 'discharge_ft3_s':'mean',\n 'status_discharge':np.unique,\n 'height_ft':'mean',\n 'status_height':np.unique,\n 'discharge_m3_per_s':'mean',\n 'height_m':'mean'})\ndf_usgs_daily.head(3)\n\n\n\n\n\n\n\n\nagency_cd\nsite_no\ntz_cd\ndischarge_ft3_s\nstatus_discharge\nheight_ft\nstatus_height\ndischarge_m3_per_s\nheight_m\n\n\ndatetime\n\n\n\n\n\n\n\n\n\n\n\n\n\n2023-04-14\n[USGS]\n[06879650]\n[UTC]\n0.0\n[A]\n2.500000\n[A]\n0.0\n8.202100\n\n\n2023-04-15\n[USGS]\n[06879650]\n[UTC]\n0.0\n[A]\n2.496667\n[A]\n0.0\n8.191164\n\n\n2023-04-16\n[USGS]\n[06879650]\n[UTC]\n0.0\n[A]\n2.494792\n[A]\n0.0\n8.185012\n\n\n\n\n\n\n\n\n# Plot hydrograph\nplt.figure(figsize=(6,4))\nplt.title('Streamflow Kings Creek Watershed')\nplt.plot(df_usgs['discharge_m3_per_s'], color='k', label='15-minute')\nplt.plot(df_usgs_daily['discharge_m3_per_s'], color='tomato', label='Daily')\nplt.yscale('log')\nplt.xlabel('Time')\nplt.ylabel('Discharge ($m^3 \\ s^{-1})$')\nplt.xticks(rotation=15)\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/request_web_data.html#example-4-u.s.-climate-reference-network", + "href": "exercises/request_web_data.html#example-4-u.s.-climate-reference-network", + "title": "44  Request web data", + "section": "Example 4: U.S. Climate reference network", + "text": "Example 4: U.S. Climate reference network\nExample of retrieving data from the U.S. Climate reference Network\n\nDaily data: https://www1.ncdc.noaa.gov/pub/data/uscrn/products/daily01/\nDaily data documentation: https://www1.ncdc.noaa.gov/pub/data/uscrn/products/daily01/README.txt\n\n\ndef get_uscrn(station, year):\n \"\"\"\n Function to retreive daily data from the U.S. Climate Reference Network.\n \n Parameters\n ----------\n station : string\n Station name\n year : integer\n Year for data request\n\n \n Returns\n -------\n df : Dataframe\n Table with daily data for an entire year for a given station\n \"\"\"\n \n url = f'https://www1.ncdc.noaa.gov/pub/data/uscrn/products/daily01/{year}/{station}'\n daily_headers = ['WBANNO','LST_DATE','CRX_VN','LONGITUDE','LATITUDE',\n 'T_DAILY_MAX','T_DAILY_MIN','T_DAILY_MEAN','T_DAILY_AVG',\n 'P_DAILY_CALC','SOLARAD_DAILY','SUR_TEMP_DAILY_TYPE',\n 'SUR_TEMP_DAILY_MAX','SUR_TEMP_DAILY_MIN','SUR_TEMP_DAILY_AVG',\n 'RH_DAILY_MAX','RH_DAILY_MIN','RH_DAILY_AVG','SOIL_MOISTURE_5_DAILY',\n 'SOIL_MOISTURE_10_DAILY','SOIL_MOISTURE_20_DAILY','SOIL_MOISTURE_50_DAILY',\n 'SOIL_MOISTURE_100_DAILY','SOIL_TEMP_5_DAILY','SOIL_TEMP_10_DAILY',\n 'SOIL_TEMP_20_DAILY','SOIL_TEMP_50_DAILY','SOIL_TEMP_100_DAILY'] \n\n # Read fixed width data\n df = pd.read_fwf(url, names=daily_headers)\n\n # Convert date from string to datetime format\n df['LST_DATE'] = pd.to_datetime(df['LST_DATE'],format='%Y%m%d')\n\n # Replace missing values (-99 and -9999)\n df = df.replace([-99,-9999,999], np.nan)\n return df\n\n\n# URL link and header variables\nyear = 2018\nstation = 'CRND0103-2018-KS_Manhattan_6_SSW.txt'\n\ndf_uscrn = get_uscrn(year, station)\ndf_uscrn.head()\n\n\n\n\n\n\n\n\nWBANNO\nLST_DATE\nCRX_VN\nLONGITUDE\nLATITUDE\nT_DAILY_MAX\nT_DAILY_MIN\nT_DAILY_MEAN\nT_DAILY_AVG\nP_DAILY_CALC\n...\nSOIL_MOISTURE_5_DAILY\nSOIL_MOISTURE_10_DAILY\nSOIL_MOISTURE_20_DAILY\nSOIL_MOISTURE_50_DAILY\nSOIL_MOISTURE_100_DAILY\nSOIL_TEMP_5_DAILY\nSOIL_TEMP_10_DAILY\nSOIL_TEMP_20_DAILY\nSOIL_TEMP_50_DAILY\nSOIL_TEMP_100_DAILY\n\n\n\n\n0\n53974\n2018-01-01\n2.422\n-96.61\n39.1\n-11.0\n-23.4\n-17.2\n-17.1\n0.0\n...\nNaN\n0.114\nNaN\nNaN\nNaN\n-2.7\n-0.8\n0.8\nNaN\nNaN\n\n\n1\n53974\n2018-01-02\n2.422\n-96.61\n39.1\n-4.4\n-20.8\n-12.6\n-11.6\n0.0\n...\nNaN\n0.106\nNaN\nNaN\nNaN\n-2.5\n-1.0\n0.1\nNaN\nNaN\n\n\n2\n53974\n2018-01-03\n2.422\n-96.61\n39.1\n-1.5\n-13.3\n-7.4\n-6.1\n0.0\n...\nNaN\n0.105\nNaN\nNaN\nNaN\n-1.8\n-0.7\n-0.1\nNaN\nNaN\n\n\n3\n53974\n2018-01-04\n2.422\n-96.61\n39.1\n3.2\n-16.3\n-6.5\n-6.5\n0.0\n...\nNaN\n0.102\nNaN\nNaN\nNaN\n-1.9\n-0.8\n-0.2\nNaN\nNaN\n\n\n4\n53974\n2018-01-05\n2.422\n-96.61\n39.1\n-0.6\n-11.9\n-6.2\n-6.7\n0.0\n...\nNaN\n0.102\nNaN\nNaN\nNaN\n-1.6\n-0.6\n-0.1\nNaN\nNaN\n\n\n\n\n5 rows × 28 columns" + }, + { + "objectID": "exercises/soil_water_storage.html#trapezoidal-integration", + "href": "exercises/soil_water_storage.html#trapezoidal-integration", + "title": "45  Profile water storage", + "section": "Trapezoidal integration", + "text": "Trapezoidal integration\nBefore calculating the soil water storage for all dates we will first compute the storage for a single date to ensure our calculations are correct.\nThe trapezoidal rule is a discrete integration method that basically adds up a collection of trapezoids. The narrower the intervals the more accurate the method, particularly when dealing with sudden non-linear changes.\n\nFigure: An animated gif showing how the progressive reduction in step size increases the accuracy of the approximated area below the function. Khurram Wadee (2014). This file is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported license.\n\nvwc_1 = df[\"7/2/2009\"].values # volumetric water content\nstorage_1 = np.trapz(vwc_1, depths) # total profile soil water storage in cm\n\nprint(storage_1,\"cm of water in 2-Jul-2009\")\n\n39.77 cm of water in 2-Jul-2009\n\n\n\nvwc_2 = df[\"7/10/2009\"].values # volumetric water content\nstorage_2 = np.trapz(vwc_2, depths) # total profile soil water storage in cm\n\nprint(storage_2,\"cm of water in 10-Jul-2009\")\n\n43.03 cm of water in 10-Jul-2009\n\n\n\n# Plot profile\nplt.figure(figsize=(4,4))\nplt.plot(vwc_1,depths*-1, '-k', label=\"2-Jul-2009\")\nplt.plot(vwc_2,depths*-1, '--k', label=\"10-Jul-2009\")\nplt.xlabel('Volumetric water content (cm$^3$ cm$^{-3}$)')\nplt.ylabel('Soil depth (cm)')\nplt.legend()\nplt.fill_betweenx(depths*-1, vwc_1, vwc_2, facecolor=(0.7,0.7,0.7), alpha=0.25)\nplt.xlim(0,0.5)\nplt.show()\n\n\n\n\n\n# Compute total soil water storage for each date\nstorage = np.array([])\nfor date in range(1,len(df.columns)):\n storage_date = np.round(np.trapz(df.iloc[:,date], depths), 2)\n storage = np.append(storage,storage_date)\n \nstorage\n\narray([39.77, 43.03, 42.84, 44.93, 44.9 , 45.57, 48.42, 55.03, 53.09,\n 52.92, 51.87, 51.24, 51.2 , 54.2 , 53.82, 54.4 , 53.93, 53.71,\n 52.37, 51.67, 51.57, 52.91, 48.94, 48.38, 46.59, 42.89, 47.29,\n 47.61, 45.1 , 45.69, 51.08, 50.9 , 49.9 , 53.62, 52.32, 52.53,\n 53.1 , 52.12, 55.43, 54. , 53.05, 51.87, 49.45, 50.56, 48.79,\n 49.66, 49.49, 49.36, 45.03, 41.13, 40.79, 41.34, 42.24, 43.95,\n 43.79, 43.23, 44.51, 43.31, 42.84, 43.64, 46.85, 45.5 ])\n\n\n\n# Get measurement dates and convert them to datetime format\nobs_dates = pd.to_datetime(df.columns[1:], format=\"%m/%d/%Y\") # Skip first column with depths\nobs_delta = obs_dates - obs_dates[0]\nobs_seq = obs_delta.days\nprint(len(obs_seq))\n\n62\n\n\n\n# Plot timeseries of profile soil moisture\nplt.figure(figsize=(6,3))\nplt.plot(obs_dates, storage, color='k', marker='o')\nplt.gca().xaxis.set_major_formatter(fmt_dates)\nplt.ylabel('Storage (cm)')\nplt.show()\n\n\n\n\n\n# X values (days since the beginning of data collection)\nx = np.tile(obs_seq,10)\nx.shape\n\n(620,)\n\n\n\n# Add number of days since 1-Jan-1970\n# This allows us to keep integers for the contour and then conver them to dates\nx += (obs_dates[0] - pd.to_datetime('1970-01-01')).days\n\n\n# Y values\ny = np.repeat(depths*-1,62)\ny.shape\n\n(620,)\n\n\n\n# Z values\nz = df.iloc[:,1:].values.flatten()\nz.shape\n\n(620,)" + }, + { + "objectID": "exercises/soil_water_storage.html#contour-plot", + "href": "exercises/soil_water_storage.html#contour-plot", + "title": "45  Profile water storage", + "section": "Contour plot", + "text": "Contour plot\n\nplt.figure(figsize=(18,4))\nplt.tricontour(x, y, z, levels=14, linewidths=0.5, colors='k')\nplt.tricontourf(x, y, z, levels=14, cmap=\"RdBu\")\nplt.xticks(fontsize=16)\nplt.colorbar(label=\"Volumetric Water Content\")\nplt.ylabel('Soil depth (cm)', fontsize=16)\nplt.yticks(fontsize=16)\n\nplt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))\n\nplt.show()" + }, + { + "objectID": "exercises/soil_water_storage.html#references", + "href": "exercises/soil_water_storage.html#references", + "title": "45  Profile water storage", + "section": "References", + "text": "References\nPatrignani, A., Godsey, C.B., Ochsner, T.E. and Edwards, J.T., 2012. Soil water dynamics of conventional and no-till wheat in the Southern Great Plains. Soil Science Society of America Journal, 76(5), pp.1768-1775.\nYimam, Y.T., Ochsner, T.E., Kakani, V.G. and Warren, J.G., 2014. Soil water dynamics and evapotranspiration under annual and perennial bioenergy crops. Soil Science Society of America Journal, 78(5), pp.1584-1593." + }, + { + "objectID": "exercises/plant_available_water.html#integral-energy", + "href": "exercises/plant_available_water.html#integral-energy", + "title": "46  Plant Available Water", + "section": "Integral energy", + "text": "Integral energy\nThe integral energy approach aims at characterizing the total amount of work required to extract a given amount of water from the soil. This approach can be useful to better understand plant responses to soil water stress since it does not assume equal availability of water between two potentials like the traditional available water capacity approach.\n E_i = \\int_{\\theta_i}^{\\theta_f} \\frac{1}{\\theta_i - \\theta_f} \\psi(\\theta) \\; d\\theta\nWe will use the soil water retention model proposed by van Genuchten (1980) since it is the most familiar for students in soil science.\nWe will also use the clay and silty clay soil in the original manuscript published by Minasny and McBratney 2003 as an example. The soil water retention curves of these two soils have similar values of volumetric water content at -10 J/kg and -1500 J/kg, but the concavity between these two points is different, which should result in similar plant available water using the traditional approach, but different amount of work between these two points using the integral energy approach.\n\n# Import modules\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\n# Define soil water retention model (van Genuchten 1980)\nmodel = lambda x,alpha,n,theta_r,theta_s: theta_r+(theta_s-theta_r)*(1+(alpha*x)**n)**-(1-1/n)\n\n\n# Range in matric potential\nfc = 10 # Field capacity (J/kg)\nwp = 1500 # Wilting point (J/kg)\nN = 10000\n\n# Define absolute values of matric potential\nmatric = np.logspace(np.log10(fc), np.log10(wp), N) \n\n\n# Krasnozem clay (Table 1 Minasny and McBratney, 2003)\ntheta_clay = model(matric, 1.22, 1.34, 0.23, 0.64)\n\n# Voluemtric water content values at filed capacity and wilting point\nfc_clay = theta_clay[0] # Field capacity, upper limit\nwp_clay = theta_clay[-1] # Wilting point, lower limit\n\n# Plant available water\npaw_clay = fc_clay - wp_clay\n\nprint('Clay at -10 J/kg:', round(fc_clay,3))\nprint('Clay at -1500 J/kg:', round(wp_clay,3))\nprint('Clay Plant Available Water Capacity',round(paw_clay,3), \"cm^3/cm^3\")\n\nClay at -10 J/kg: 0.404\nClay at -1500 J/kg: 0.262\nClay Plant Available Water Capacity 0.142 cm^3/cm^3\n\n\n\n# Xanthozem silty-clay (Table 1 Minasny and McBratney, 2003)\ntheta_silty_clay = model(matric, 0.05, 1.1, 0, 0.42)\n\n# Voluemtric water content values at filed capacity and wilting point\nfc_silty_clay = theta_silty_clay[0] # Field capacity, upper limit\nwp_silty_clay = theta_silty_clay[-1] # Wilting point, lower limit\n\n# Plant available water\npaw_silty_clay = fc_silty_clay - wp_silty_clay\n\nprint('Silty-clay at -10 J/kg:', round(fc_silty_clay,3))\nprint('Silty-clay at -1500 J/kg:', round(wp_silty_clay,3))\nprint('Silty clay Plant Available Water Capacity',round(paw_silty_clay,3), \"cm^3/cm^3\")\n\nSilty-clay at -10 J/kg: 0.406\nSilty-clay at -1500 J/kg: 0.273\nSilty clay Plant Available Water Capacity 0.133 cm^3/cm^3\n\n\n\nplt.figure(figsize=(8,6))\nplt.plot(np.log10(matric), theta_clay, '--b', label='Clay')\nplt.plot(np.log10(matric), theta_silty_clay, '-k', label='Silty clay')\nplt.xlabel(\"Matric potential $|\\psi_m|$ (J/kg)\", size=16)\nplt.ylabel(\"Volumetric water content (cm$^3$/cm$^3$)\", size=16)\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/plant_available_water.html#soil-water-storage", + "href": "exercises/plant_available_water.html#soil-water-storage", + "title": "46  Plant Available Water", + "section": "Soil water storage", + "text": "Soil water storage\n\n# Total storage clay between -10 and -1500 J/kg\nW_clay = 1/np.abs(fc - wp) * np.trapz(theta_clay, matric)\nprint(W_clay)\n\n0.2768349634688867\n\n\n\n# Total storage silty clay between -10 and -1500 J/kg\nstorage_silty_clay = 1/np.abs(fc - wp) * np.trapz(theta_silty_clay, matric)\nprint(storage_silty_clay)\n\n0.3001711916723065" + }, + { + "objectID": "exercises/plant_available_water.html#energy", + "href": "exercises/plant_available_water.html#energy", + "title": "46  Plant Available Water", + "section": "Energy", + "text": "Energy\nBecause theta in our code is in decreasing order, the \\Delta x during the trapezoidal integration will result in negative values. So, I swapped the minuend and subtrahend from \\theta_i - \\theta_f to \\theta_f - \\theta_i to reverse the sign.\n\n# Integral energy\ntheta_i = theta_clay[0]\ntheta_f = theta_clay[-1]\nE_clay = 1/(theta_f - theta_i) * np.trapz(matric, x=theta_clay)\nprint(round(E_clay*-1), 'J/kg are required to go from FC to WP')\n\n-167.0 J/kg are required to go from FC to WP\n\n\n\ntheta_i = theta_silty_clay[0]\ntheta_f = theta_silty_clay[-1]\nE_silty_clay = 1/(theta_f - theta_i) * np.trapz(matric, x=theta_silty_clay)\nprint(round(E_silty_clay*-1), 'J/kg are required to go from FC to WP')\n\n-319.0 J/kg are required to go from FC to WP" + }, + { + "objectID": "exercises/plant_available_water.html#references", + "href": "exercises/plant_available_water.html#references", + "title": "46  Plant Available Water", + "section": "References", + "text": "References\nGroenevelt, P.H., Grant, C.D. and Semetsa, S., 2001. A new procedure to determine soil water availability. Soil Research, 39(3), pp\nHendrickson, A.H. and Veihmeyer, F.J., 1945. Permanent wilting percentages of soils obtained from field and laboratory trials. Plant physiology, 20(4), p.517.\nMinasny, B. and McBratney, A.B., 2003. Integral energy as a measure of soil-water availability. Plant and Soil, 249(2), pp.253-262.\nVeihmeyer, F.J. and Hendrickson, A.H., 1931. The moisture equivalent as a measure of the field capacity of soils. Soil Science, 32(3), pp.181-194." + }, + { + "objectID": "exercises/photoperiod.html#define-photoperiod-function", + "href": "exercises/photoperiod.html#define-photoperiod-function", + "title": "47  Photoperiod", + "section": "Define photoperiod function", + "text": "Define photoperiod function\nFor this type of applications is convenient to have a function that we can invoke to estimate the daylight hours for any latitude and day of the year. Since we are going to use the Numpy module, we could potentially pass multiple days of the year and estimate the photoperiod along the year for a single location.\n\n# Define function\ndef photoperiod(phi, doy, verbose=False):\n \"\"\"\n Function to compute photoperiod or daylight hours. This function is not accurate\n near polar regions.\n \n Parameters:\n phi(integer, float): Latitude in decimal degrees. Northern hemisphere is positive.\n doy(integer): Day of the year (days since January 1)\n\n Returns:\n float: photoperiod or daylight hours.\n \n References:\n Keisling, T.C., 1982. Calculation of the Length of Day 1. Agronomy Journal, 74(4), pp.758-759.\n \"\"\"\n \n # Convert latitude to radians\n phi = np.radians(phi)\n \n # Angle of the sun below the horizon. Civil twilight is -4.76 degrees.\n light_intensity = 2.206 * 10**-3\n B = -4.76 - 1.03 * np.log(light_intensity) # Eq. [5].\n\n # Zenithal distance of the sun in degrees\n alpha = np.radians(90 + B) # Eq. [6]. Value at sunrise and sunset.\n \n # Mean anomaly of the sun. It is a convenient uniform measure of \n # how far around its orbit a body has progressed since pericenter.\n M = 0.9856*doy - 3.251 # Eq. [4].\n \n # Declination of sun in degrees\n lmd = M + 1.916*np.sin(np.radians(M)) + 0.020*np.sin(np.radians(2*M)) + 282.565 # Eq. [3]. Lambda\n C = np.sin(np.radians(23.44)) # 23.44 degrees is the orbital plane of Earth around the Sun\n delta = np.arcsin(C*np.sin(np.radians(lmd))) # Eq. [2].\n\n # Calculate daylength in hours, defining sec(x) = 1/cos(x)\n P = 2/15 * np.degrees( np.arccos( np.cos(alpha) * (1/np.cos(phi)) * (1/np.cos(delta)) - np.tan(phi) * np.tan(delta) ) ) # Eq. [1].\n\n # Print results in order for each computation to match example in paper\n if verbose:\n print('Input latitude =', np.degrees(phi))\n print('[Eq 5] B =', B)\n print('[Eq 6] alpha =', np.degrees(alpha))\n print('[Eq 4] M =', M[0])\n print('[Eq 3] Lambda =', lmd[0])\n print('[Eq 2] delta=', np.degrees(delta[0]))\n print('[Eq 1] Daylength =', P[0])\n \n return P" + }, + { + "objectID": "exercises/photoperiod.html#example-1-single-latitude-and-single-doy", + "href": "exercises/photoperiod.html#example-1-single-latitude-and-single-doy", + "title": "47  Photoperiod", + "section": "Example 1: Single latitude and single DOY", + "text": "Example 1: Single latitude and single DOY\nNow that we have the function ready, we can compute the daylight hours for a specific latitude and day of the year. To test the code we will use the example provided in the manuscript by Keisling, 1982.\n\n# Invoke function with scalars\nphi = 33.4 # Latitude\ndoy = np.array([201]) # Day of the year.\n\n# Calculate photoperiod\nP = photoperiod(phi,doy,verbose=True)\nprint('Photoperiod: ' + str(np.round(P[0],2)) + ' hours/day')\n\nInput latitude = 33.4\n[Eq 5] B = 1.5400715888953513\n[Eq 6] alpha = 91.54007158889536\n[Eq 4] M = 194.8546\n[Eq 3] Lambda = 476.93831283687416\n[Eq 2] delta= 20.770548026002125\n[Eq 1] Daylength = 14.203998218048154\nPhotoperiod: 14.2 hours/day" + }, + { + "objectID": "exercises/photoperiod.html#example-2-single-latitude-for-a-range-of-doy", + "href": "exercises/photoperiod.html#example-2-single-latitude-for-a-range-of-doy", + "title": "47  Photoperiod", + "section": "Example 2: Single latitude for a range of DOY", + "text": "Example 2: Single latitude for a range of DOY\n\n# Estiamte photperiod for single latitude and the entire year\nphi = 33.4\ndoy = np.arange(1,365)\nP = photoperiod(phi,doy)\n\n\n# Create figure of single Lat for the whole year\nplt.figure(figsize=(6,4))\nplt.plot(doy, P, color='k')\nplt.title('Latitude:' + str(phi))\nplt.xlabel('Day of the year', size=14)\nplt.ylabel('Photoperiod (hours per day)', size=14)\nplt.show()" + }, + { + "objectID": "exercises/photoperiod.html#example-3-single-doy-for-a-range-of-latitudes", + "href": "exercises/photoperiod.html#example-3-single-doy-for-a-range-of-latitudes", + "title": "47  Photoperiod", + "section": "Example 3: Single DOY for a range of latitudes", + "text": "Example 3: Single DOY for a range of latitudes\n\n# Estiamte photperiod for single latitude and the entire year\nlats = np.linspace(0,40)\nP_doy1 = photoperiod(lats, 1)\nP_doy180 = photoperiod(lats, 180)\n\n\n# Create figure for single day of the year for a range of latitudes\nplt.figure(figsize=(6,4))\nplt.plot(lats,P_doy1, color='k', label='DOY 1')\nplt.plot(lats,P_doy180, color='k', linestyle='--', label='DOY 180')\nplt.xlabel('Latitude (decimal degrees)', size=14)\nplt.ylabel('Photoperiod (hours per day)', size=14)\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/photoperiod.html#references", + "href": "exercises/photoperiod.html#references", + "title": "47  Photoperiod", + "section": "References", + "text": "References\nKeisling, T.C., 1982. Calculation of the Length of Day 1. Agronomy Journal, 74(4), pp.758-759." + }, + { + "objectID": "exercises/curve_number.html#practice", + "href": "exercises/curve_number.html#practice", + "title": "48  Runoff", + "section": "Practice", + "text": "Practice\n\nUsing precipitation observations for 2007, what is the total runoff for a fallow under bare soil for a soil with hydrologic condition D?\nSelect a year in which the total runoff is lower than for 2007. Use a curve number of 80.\nModify the curve number function so that it works with precipitation data in both inches and millimeters." + }, + { + "objectID": "exercises/curve_number.html#references", + "href": "exercises/curve_number.html#references", + "title": "48  Runoff", + "section": "References", + "text": "References\nPonce, V.M. and Hawkins, R.H., 1996. Runoff curve number: Has it reached maturity?. Journal of hydrologic engineering, 1(1), pp.11-19." + }, + { + "objectID": "exercises/air_temperature_model.html#basics-of-waves", + "href": "exercises/air_temperature_model.html#basics-of-waves", + "title": "49  Air temperature model", + "section": "Basics of waves", + "text": "Basics of waves\n\ndoy = np.arange(1, 366)\nwave_model = lambda x,a,b,c: a + b * np.cos(2*np.pi*(x-c)/365 + np.pi)\n\n\ndoy = np.arange(1,366,1)\n\nplt.figure(figsize=(12,3))\n\nplt.subplot(1,3,1)\nplt.title('Change mean value')\nplt.plot(doy, wave_model(doy, a=10, b=20, c=1), '-k')\nplt.plot(doy, wave_model(doy, a=25, b=20, c=1), '--r')\nplt.xlabel('Day of the year')\n\nplt.subplot(1,3,2)\nplt.title('Change amplitude value')\nplt.plot(doy, wave_model(doy, a=25, b=20, c=1), '-k')\nplt.plot(doy, wave_model(doy, a=25, b=5, c=20), '--r')\nplt.xlabel('Day of the year')\n\nplt.subplot(1,3,3)\nplt.title('Change offset value')\nplt.plot(doy, wave_model(doy, a=25, b=5, c=0), '-k')\nplt.plot(doy, wave_model(doy, a=25, b=5, c=50), '--r')\nplt.xlabel('Day of the year')\n\nplt.show()" + }, + { + "objectID": "exercises/air_temperature_model.html#load-and-inspect-data", + "href": "exercises/air_temperature_model.html#load-and-inspect-data", + "title": "49  Air temperature model", + "section": "Load and inspect data", + "text": "Load and inspect data\n\n# Read dataset\ndf = pd.read_csv('../datasets/KS_Manhattan_6_SSW.csv',\n parse_dates=['LST_DATE'], date_format='%Y%m%d', na_values=[-9999,-99])\n\n# Retain columns of interest\ndf = df[['LST_DATE','T_DAILY_AVG']]\n\n# Add year and day of the year columns\ndf['DOY'] = df['LST_DATE'].dt.dayofyear\ndf['YEAR'] = df['LST_DATE'].dt.year\n\n# Inspect a few rows\ndf.head(3)\n\n\n\n\n\n\n\n\nLST_DATE\nT_DAILY_AVG\nDOY\nYEAR\n\n\n\n\n0\n2003-10-01\nNaN\n274\n2003\n\n\n1\n2003-10-02\n11.7\n275\n2003\n\n\n2\n2003-10-03\n14.8\n276\n2003\n\n\n\n\n\n\n\n\n# Inspect complete air temperature data to see seasonal pattern\nplt.figure(figsize=(8,4))\nplt.plot(df['LST_DATE'], df['T_DAILY_AVG'], linewidth=0.5)\nplt.ylabel('Air temperature (Celsius)')\nplt.show()" + }, + { + "objectID": "exercises/air_temperature_model.html#split-dataset-into-training-and-testing-sets", + "href": "exercises/air_temperature_model.html#split-dataset-into-training-and-testing-sets", + "title": "49  Air temperature model", + "section": "Split dataset into training and testing sets", + "text": "Split dataset into training and testing sets\nIn this tutorial we are using the train set to estimate the model parameters. We then make predictions of average air temperature using these parameters in both the train and test sets.\n\n# Split set into training and test\nN = round(df.shape[0]*0.75)\ndf_train = df[:N]\ndf_test = df[N+1:].reset_index(drop=True)" + }, + { + "objectID": "exercises/air_temperature_model.html#define-model-for-air-temperature", + "href": "exercises/air_temperature_model.html#define-model-for-air-temperature", + "title": "49  Air temperature model", + "section": "Define model for air temperature", + "text": "Define model for air temperature\n\n# Daily air temperature model\nmodel = lambda doy,a,b,c: a + b * np.cos(2*np.pi*( (doy-c)/365) + np.pi)" + }, + { + "objectID": "exercises/air_temperature_model.html#estimate-model-parameters-from-data", + "href": "exercises/air_temperature_model.html#estimate-model-parameters-from-data", + "title": "49  Air temperature model", + "section": "Estimate model parameters from data", + "text": "Estimate model parameters from data\n\n# Annual mean temperature\nT_avg = df['T_DAILY_AVG'].mean()\nprint(f'Mean annual temperature: {T_avg:.2f}')\n\nMean annual temperature: 13.09\n\n\n\n# Annual mean thermal amplitude\nT_min,T_max = df_train.groupby(by='DOY')[\"T_DAILY_AVG\"].mean().quantile([0.05,0.95])\nA = (T_max - T_min)/2\nprint(f'Mean annual thermal amplitude: {A:.2f}')\n\nMean annual thermal amplitude: 14.36\n\n\n\n# Timing of minimum air temperature\nidx_min = df_train.groupby(by='YEAR')['T_DAILY_AVG'].idxmin()\ndoy_T_min = np.round(df_train.loc[idx_min,'DOY'].apply(circmean).mean())\nprint(f'DOY of minimum temperature (phase constant): {doy_T_min}')\n\nDOY of minimum temperature (phase constant): 3.0" + }, + { + "objectID": "exercises/air_temperature_model.html#predict-air-temperature-for-training-set", + "href": "exercises/air_temperature_model.html#predict-air-temperature-for-training-set", + "title": "49  Air temperature model", + "section": "Predict air temperature for training set", + "text": "Predict air temperature for training set\n\n# Predict daily T with model\nT_pred_train = model(df_train['DOY'], T_avg, A, doy_T_min)\n\n\n# Compute mean absolute error on train set\nmae_train = np.mean(np.abs(df_train['T_DAILY_AVG'] - T_pred_train)) \n\nprint(f'MAE on train set: {mae_train:.2f} Celsius')\n\nMAE on train set: 4.53 Celsius\n\n\n\n# Create figure\nplt.figure(figsize=(8,4))\nplt.scatter(df_train[\"LST_DATE\"], df_train[\"T_DAILY_AVG\"], s=5, color='gray', label=\"Observed\")\nplt.plot(df_train[\"LST_DATE\"],T_pred_train, label=\"Predicted\", color='tomato', linewidth=1)\nplt.ylabel(\"Air temperature (Celsius)\")\nplt.grid()\nplt.show()" + }, + { + "objectID": "exercises/air_temperature_model.html#predict-daily-average-air-temperature-for-test-set", + "href": "exercises/air_temperature_model.html#predict-daily-average-air-temperature-for-test-set", + "title": "49  Air temperature model", + "section": "Predict daily average air temperature for test set", + "text": "Predict daily average air temperature for test set\nIn this step we use the same parameters determined using the training set.\n\n# Predict daily T with model\nT_pred_test = model(df_test['DOY'], T_avg, A, doy_T_min)\n\n\n# Compute mean absolute error on test set\nmae_test = np.mean(np.abs(df_test['T_DAILY_AVG'] - T_pred_test)) \n\nprint(f'MAE on test set: {mae_test:.2f} Celsius')\n\nMAE on test set: 4.54 Celsius\n\n\n\n# Create figure\nplt.figure(figsize=(8,4))\nplt.scatter(df_test[\"LST_DATE\"], df_test[\"T_DAILY_AVG\"], s=5, color='gray', label=\"Observed\")\nplt.plot(df_test[\"LST_DATE\"],T_pred_test, label=\"Predicted\", color='tomato', linewidth=1)\nplt.ylabel(\"Air temperature (Celsius)\")\nplt.grid()\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/air_temperature_model.html#practice", + "href": "exercises/air_temperature_model.html#practice", + "title": "49  Air temperature model", + "section": "Practice", + "text": "Practice\n\nUse the curve_fit() function from SciPy to fit the model and compare parameters values with this exercise.\nRead a dataset of hourly observations of mean air temperature and fit a model with an additional harmonic to simulate diurnal thermal osciallations in addition to the seasonal oscillations." + }, + { + "objectID": "exercises/soil_temperature_model.html#model", + "href": "exercises/soil_temperature_model.html#model", + "title": "50  Soil Temperature Model", + "section": "Model", + "text": "Model\n T(z,t) = T_{avg} + A \\ e^{-z/d} \\ sin(\\omega t - z/d - \\phi)\nT is the soil temperature at time t and depth z\nz is the soil depth in meters\nt is the time in days of year\nT_{avg} is the annual average temperature at the soil surface\nA is the thermal amplitude: (T_{max} + T_{min})/2.\n\\omega is the angular frequency: 2\\pi / P\nP is the period. Should be in the same units as t. The period is 365 days for annual oscillations and 24 hours for daily oscillations.\n\\phi is the phase constant, which is defined as: \\frac{\\pi}{2} + \\omega t_0\nt_0 is the time lag from an arbitrary starting point. In this case are days from January 1.\nd is the damping depth, which is defined as: \\sqrt{(2 D/ \\omega)}. It has length units.\nD is thermal diffusivity in m^2 d^{-1}. The thermal diffusivity is defined as \\kappa / C\n\\kappa is the soil thermal conductivity in J m^{-1} K^{-1} d^{-1}\nC is the soil volumetric heat capacity in J m^{-3} K^{-1}" + }, + { + "objectID": "exercises/soil_temperature_model.html#assumptions", + "href": "exercises/soil_temperature_model.html#assumptions", + "title": "50  Soil Temperature Model", + "section": "Assumptions", + "text": "Assumptions\n\nConstant soil thermal diffusivity.\nUniform soil texture\nTemperature in deep layers approximate the mean annual air temperature\nIn situation where we don’t have observations of soil temperature at the surface we also assume that the soil surface temperature is equal to the air temperature.\n\n\n# Import modules\nimport numpy as np\nimport math\nimport matplotlib.pyplot as plt\nfrom mpl_toolkits.mplot3d import Axes3D" + }, + { + "objectID": "exercises/soil_temperature_model.html#model-inputs", + "href": "exercises/soil_temperature_model.html#model-inputs", + "title": "50  Soil Temperature Model", + "section": "Model inputs", + "text": "Model inputs\n\n# Constants\nT_avg = 25 # Annual average temperature at the soil surface\nA0 = 10 # Annual thermal amplitude at the soil surface\nD = 0.203 # Thermal diffusivity obtained from KD2 Pro instrument [mm^2/s]\nD = D / 100 * 86400 # convert to cm^2/day\nperiod = 365 # days\nomega = 2*np.pi/period\nt_0 = 15 # Time lag in days from January 1\nphi = np.pi/2 + omega*t_0 # Phase constant\nd = (2*D/omega)**(1/2) # Damping depth \nD\n\n175.39200000000002" + }, + { + "objectID": "exercises/soil_temperature_model.html#define-model", + "href": "exercises/soil_temperature_model.html#define-model", + "title": "50  Soil Temperature Model", + "section": "Define model", + "text": "Define model\n\n# Define model as lambda function\nT_soilfn = lambda doy,z: T_avg + A0 * np.exp(-z/d) * np.sin(omega*doy - z/d - phi)" + }, + { + "objectID": "exercises/soil_temperature_model.html#soil-temperature-for-a-specific-depth-as-a-function-of-time", + "href": "exercises/soil_temperature_model.html#soil-temperature-for-a-specific-depth-as-a-function-of-time", + "title": "50  Soil Temperature Model", + "section": "Soil temperature for a specific depth as a function of time", + "text": "Soil temperature for a specific depth as a function of time\n\ndoy = np.arange(1,366)\nz = 0\nT_soil = T_soilfn(doy,z)\n\n# Plot\nplt.figure()\nplt.plot(doy,T_soil)\nplt.show()" + }, + { + "objectID": "exercises/soil_temperature_model.html#soil-temperature-for-a-specific-day-of-the-year-as-a-function-of-depth", + "href": "exercises/soil_temperature_model.html#soil-temperature-for-a-specific-day-of-the-year-as-a-function-of-depth", + "title": "50  Soil Temperature Model", + "section": "Soil temperature for a specific day of the year as a function of depth", + "text": "Soil temperature for a specific day of the year as a function of depth\n\ndoy = 10\nNz = 100 # Number of interpolation\nzmax = 500 # cm\nz = np.linspace(0,zmax,Nz) \nT = T_soilfn(doy,z)\n\nplt.figure()\nplt.plot(T,-z)\nplt.show()" + }, + { + "objectID": "exercises/soil_temperature_model.html#soil-temperature-as-a-function-of-both-doy-and-depth", + "href": "exercises/soil_temperature_model.html#soil-temperature-as-a-function-of-both-doy-and-depth", + "title": "50  Soil Temperature Model", + "section": "Soil temperature as a function of both DOY and depth", + "text": "Soil temperature as a function of both DOY and depth\n\ndoy = np.arange(1,366)\nz = np.linspace(0,500,1000) \ndoy_grid,z_grid = np.meshgrid(doy,z)\n\n# Predict soil temperature for each grid\nT_grid = T_soilfn(doy_grid,z_grid)\n\n\n# Create figure\nfig = plt.figure(figsize=(10, 6), dpi=80) # 10 inch by 6 inch dpi = dots per inch\n\n# Get figure axes and convert it to a 3D projection\nax = fig.gca(projection='3d')\n\n# Add surface plot to axes. Save this surface plot in a variable\nsurf = ax.plot_surface(doy_grid, z_grid, T_grid, cmap='viridis', antialiased=False)\n\n# Add colorbar to figure based on ranges in the surf map.\nfig.colorbar(surf, shrink=0.5, aspect=20)\n\n# Wire mesh\nframe = surf = ax.plot_wireframe(doy_grid, z_grid, T_grid, linewidth=0.5, color='k', alpha=0.5)\n\n# Label x,y, and z axis\nax.set_xlabel(\"Day of the year\")\nax.set_ylabel('Soil depth [cm]')\nax.set_zlabel('Soil temperature \\N{DEGREE SIGN}C')\n\n# Set position of the 3D plot\nax.view_init(elev=30, azim=35) # elevation and azimuth. Change their value to see what happens.\n\nplt.show()" + }, + { + "objectID": "exercises/soil_temperature_model.html#interactive-plots", + "href": "exercises/soil_temperature_model.html#interactive-plots", + "title": "50  Soil Temperature Model", + "section": "Interactive plots", + "text": "Interactive plots\n\nfrom bokeh.plotting import figure, show, output_notebook, ColumnDataSource, save\nfrom bokeh.layouts import row\nfrom bokeh.models import HoverTool\nfrom bokeh.io import export_svgs\noutput_notebook()\n\n\n \n \n Loading BokehJS ...\n \n\n\n\n\n\n\n\n# Set data for p1\ndoy = np.arange(1,366)\nz = 0\nsource_p1 = ColumnDataSource(data=dict(x=doy, y=T_soilfn(doy,z)))\n\n# Define tools for p1\nhover_p1 = HoverTool(\n tooltips=[\n (\"Time (days)\", \"@x{0.}\"),\n (\"Temperature (Celsius)\",\"@y{0.00}\" )\n ]\n )\n\n# Create plots\np1 = figure(y_range=[0,50],\n width=400,\n height=300,\n title=\"Soil Temperature as a Function of Time\",\n tools=[hover_p1],\n toolbar_location=\"right\")\n\np1.xaxis.axis_label = 'Time [hours]'\np1.yaxis.axis_label = 'Temperature'\np1.line('x','y',source=source_p1)\n\n\n# Set data for p2\ndoy = 150\nz = np.linspace(0,500,100)\nsource_p2 = ColumnDataSource(data=dict(y=-1*z, x=T_soilfn(150,z)))\n\n# Define tools for p1\nhover_p2 = HoverTool(\n tooltips=[\n (\"Depth (cm)\",\"@y{0.0}\"),\n (\"Temperature (Celsius)\",\"@x{0.00}\")\n ]\n )\n\n# Create plots\np2 = figure(y_range=[0,-500],\n width=400,\n height=300,\n title=\"Soil Temperature as a Function of Soil Depth\",\n tools=[hover_p2],\n toolbar_location=\"right\")\n\np2.xaxis.axis_label = 'Temperature'\np2.yaxis.axis_label = 'Depth (cm)'\np2.min_border_left = 100\np2.line('x','y',source=source_p2)\n\np1.output_backend = \"svg\"\np2.output_backend = \"svg\"\n\nshow(row(p1,p2))" + }, + { + "objectID": "exercises/soil_temperature_model.html#references", + "href": "exercises/soil_temperature_model.html#references", + "title": "50  Soil Temperature Model", + "section": "References", + "text": "References\nWu, J. and Nofziger, D.L., 1999. Incorporating temperature effects on pesticide degradation into a management model. Journal of Environmental Quality, 28(1), pp.92-100." + }, + { + "objectID": "exercises/thermal_time.html#example-using-non-vectorized-functions", + "href": "exercises/thermal_time.html#example-using-non-vectorized-functions", + "title": "51  Growing degree days", + "section": "Example using non-vectorized functions", + "text": "Example using non-vectorized functions\nThe following functions can only accept one value of T_avg at the time. Which means that to compute multiple values in an array we would need to implement a loop. The advantage of this method is its simplicity and clarity.\n\n# Method 1: T_base only\ndef gdd_method_1(T_avg,T_base,dt=1):\n if T_avg < T_base:\n GDD = 0\n else:\n GDD = (T_avg - T_base)*dt\n\n return GDD\n\n\n# Method 2: T_base, T_opt, and T_upper (Linear)\ndef gdd_method_2(T_avg,T_base,T_opt,T_upper,dt=1):\n if T_avg <= T_base:\n GDD = 0\n\n elif T_base < T_avg < T_opt:\n GDD = (T_avg - T_base)*dt\n\n elif T_opt <= T_avg < T_upper:\n GDD = (T_upper - T_avg)/(T_upper - T_opt)*(T_opt - T_base)*dt\n\n else:\n GDD = 0\n \n return GDD\n\n\n# Test that functions are working as expected\nT_avg = 25\n\nprint(gdd_method_1(T_avg, T_base))\nprint(gdd_method_2(T_avg, T_base, T_opt, T_upper))\n\n15\n15\n\n\n\n# Compute growing degree days\n\n# Create empty arrays to append function values\nGDD_1 = []\nGDD_2 = []\n\n# Iterate over each row\nfor k,row in df_season.iterrows():\n GDD_1.append(gdd_method_1(row['TEMP2MAVG'],T_base))\n GDD_2.append(gdd_method_2(row['TEMP2MAVG'],T_base, T_opt, T_upper)) \n \n# Add arrays as new dataframe columns\ndf_season['GDD_1'] = GDD_1\ndf_season['GDD_2'] = GDD_2\n\ndf_season['GDD_1_cum'] = df_season['GDD_1'].cumsum()\ndf_season['GDD_2_cum'] = df_season['GDD_2'].cumsum()\n\n# Display resulting dataframe (new columns are at the end)\ndf_season.head(3)\n\n\n\n\n\n\n\n\nTIMESTAMP\nSTATION\nPRESSUREAVG\nPRESSUREMAX\nPRESSUREMIN\nSLPAVG\nTEMP2MAVG\nTEMP2MMIN\nTEMP2MMAX\nTEMP10MAVG\n...\nSOILTMP20AVG655\nSOILTMP50AVG655\nVWC5CM\nVWC10CM\nVWC20CM\nVWC50CM\nGDD_1\nGDD_2\nGDD_1_cum\nGDD_2_cum\n\n\n\n\n0\n2018-04-01\nGypsum\n97.48\n98.18\n96.76\n101.93\n8.70\n0.45\n14.76\n8.40\n...\n9.98\n9.26\n0.1731\n0.1696\n0.2572\n0.2146\n0.0\n0.0\n0.0\n0.0\n\n\n1\n2018-04-02\nGypsum\n97.94\n98.23\n97.46\n102.62\n-2.30\n-4.05\n1.00\n-2.48\n...\n8.68\n9.41\n0.1690\n0.1653\n0.2545\n0.2148\n0.0\n0.0\n0.0\n0.0\n\n\n2\n2018-04-03\nGypsum\n96.42\n97.55\n95.47\n101.01\n1.34\n-3.65\n10.27\n1.20\n...\n7.03\n8.58\n0.1687\n0.1626\n0.2517\n0.2136\n0.0\n0.0\n0.0\n0.0\n\n\n\n\n3 rows × 48 columns\n\n\n\n\ndiff_methods = np.round(df_season['GDD_1_cum'].iloc[-1] - df_season['GDD_2_cum'].iloc[-1])\nprint('The difference between methods is',diff_methods, 'degree-days')\n\nThe difference between methods is 106.0 degree-days\n\n\n\n# Create figure\nplt.figure(figsize=(6,4))\n\nplt.plot(df_season['TIMESTAMP'], df_season['GDD_1'].cumsum(), '-k', label='Method 1')\nplt.plot(df_season['TIMESTAMP'], df_season['GDD_2'].cumsum(), '--k', label='Method 2')\nplt.xlabel('Date')\nplt.xticks(rotation=90)\n#plt.ylabel(u'Growing degree days (\\N{DEGREE SIGN}C-d)')\nplt.ylabel(f'Growing degree days {chr(176)}C-d)')\nplt.legend()\nfmt = mdates.DateFormatter('%d-%b')\nplt.gca().xaxis.set_major_formatter(fmt)\n\nplt.show()" + }, + { + "objectID": "exercises/thermal_time.html#example-using-vectorized-functions", + "href": "exercises/thermal_time.html#example-using-vectorized-functions", + "title": "51  Growing degree days", + "section": "Example using vectorized functions", + "text": "Example using vectorized functions\n\ndef gdd_method_1_vect(T_avg, T_base, dt=1):\n \"\"\"Vectorized function for computing GDD using method 1\"\"\"\n \n # Pre-allocate the GDD array with NaNs\n GDD = np.full_like(T_avg, np.nan)\n \n # Case 1: T_avg <= T_base\n condition_1 = T_avg <= T_base\n GDD[condition_1] = 0\n\n # Case 2: T_avg > T_base\n condition_2 = T_avg > T_base\n GDD[condition_2] = (T_avg[condition_2] - T_base)*dt\n\n return GDD\n\n\ndef gdd_method_2_vect(T_avg, T_base, T_opt, T_upper, dt=1):\n \"\"\"Vectorized function for computing GDD using method 2\"\"\"\n \n # Pre-allocate the GDD array with NaNs\n GDD = np.full_like(T_avg, np.nan)\n\n # Case 1: T_avg <= T_base\n condition_1 = T_avg <= T_base\n GDD[condition_1] = 0\n \n # Case 2: T_base < T_avg <= T_opt\n condition_2 = (T_avg > T_base) & (T_avg <= T_opt)\n GDD[condition_2] = (T_avg[condition_2] - T_base) * dt\n\n # Case 3: T_opt < T_avg <= T_upper\n condition_3 = (T_avg > T_opt) & (T_avg <= T_upper)\n GDD[condition_3] = ((T_upper-T_avg[condition_3]) / (T_upper-T_opt) * (T_opt-T_base)) * dt\n\n # Case 4: T_avg > T_upper\n condition_4 = T_avg > T_upper\n GDD[condition_4] = 0\n \n return GDD\n\n\n\n\n\n\n\nTip\n\n\n\nIn the previous functions, we have opted to pre-allocate an array with NaNs (using np.full_like(T_avg, np.nan)) to clearly distinguish between unprocessed and processed data. However, it’s also possible to pre-allocate an array of zeros (using np.zeros_like(T_avg)). This approach would automatically handle cases 1 and 4 (in method 2), where conditions result in zero values. By doing so, we reduce the number of conditional checks required, making the functions shorter. This choice is particularly useful when zeros accurately represent the desired outcome for certain conditions, contributing to more efficient and concise code.\n\n\n\n# Test that functions are working as expected\nT_avg = np.array([0,12,20,30,40])\n\nprint(gdd_method_1_vect(T_avg, T_base))\nprint(gdd_method_2_vect(T_avg, T_base, T_opt, T_upper))\n\n[ 0 2 10 20 30]\n[ 0 2 10 14 0]\n\n\n\n# Compute GDD\ndf_season['GDD_1_vect'] = gdd_method_1_vect(df_season['TEMP2MAVG'], T_base)\ndf_season['GDD_2_vect'] = gdd_method_2_vect(df_season['TEMP2MAVG'], T_base, T_opt, T_upper)\n\ndf_season['GDD_1_vect_cum'] = df_season['GDD_1'].cumsum()\ndf_season['GDD_2_vect_cum'] = df_season['GDD_2'].cumsum()\n\n# Display resulting dataframe (new columns are at the end)\ndf_season.head(3)\n\n\n\n\n\n\n\n\nTIMESTAMP\nSTATION\nPRESSUREAVG\nPRESSUREMAX\nPRESSUREMIN\nSLPAVG\nTEMP2MAVG\nTEMP2MMIN\nTEMP2MMAX\nTEMP10MAVG\n...\nVWC20CM\nVWC50CM\nGDD_1\nGDD_2\nGDD_1_cum\nGDD_2_cum\nGDD_1_vect\nGDD_2_vect\nGDD_1_vect_cum\nGDD_2_vect_cum\n\n\n\n\n0\n2018-04-01\nGypsum\n97.48\n98.18\n96.76\n101.93\n8.70\n0.45\n14.76\n8.40\n...\n0.2572\n0.2146\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n\n\n1\n2018-04-02\nGypsum\n97.94\n98.23\n97.46\n102.62\n-2.30\n-4.05\n1.00\n-2.48\n...\n0.2545\n0.2148\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n\n\n2\n2018-04-03\nGypsum\n96.42\n97.55\n95.47\n101.01\n1.34\n-3.65\n10.27\n1.20\n...\n0.2517\n0.2136\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n0.0\n\n\n\n\n3 rows × 52 columns\n\n\n\n\n# Create figure using vectorized columns\nplt.figure(figsize=(6,4))\n\nplt.plot(df_season['TIMESTAMP'], df_season['GDD_1_vect_cum'], '-k', label='Method 1')\nplt.plot(df_season['TIMESTAMP'], df_season['GDD_2_vect_cum'], '--k', label='Method 2')\nplt.xlabel('Date')\nplt.xticks(rotation=90)\nplt.ylabel(f'Growing degree days {chr(176)}C-d)')\nplt.legend()\nfmt = mdates.DateFormatter('%d-%b')\nplt.gca().xaxis.set_major_formatter(fmt)\n\nplt.show()" + }, + { + "objectID": "exercises/thermal_time.html#practice", + "href": "exercises/thermal_time.html#practice", + "title": "51  Growing degree days", + "section": "Practice", + "text": "Practice\n\nSearch in the provided references or other articles in the literature for alternative methods to compute growing degree days and implement them in Python.\nMerge the code for different methods into a single function. Add an input named method= that will allow you to specify which computation method you want to use.\nConvert the non-vectorized functions into vectorized versions using the Numpy function np.vectorize(). This option is a convenient way of vectorizing functions, but it’s not intended for efficiency. Then, compute the time it takes to compute GDD with each function implementation (non-vectorized, vectorized using Numpy booleans, and vectorized using np.vectorize(). Which is one is faster? Which one is faster to code? Hint: For timing the functions use the perf_counter() method the time module." + }, + { + "objectID": "exercises/thermal_time.html#references", + "href": "exercises/thermal_time.html#references", + "title": "51  Growing degree days", + "section": "References", + "text": "References\nMcMaster, G.S. and Wilhelm, W., 1997. Growing degree-days: one equation, two interpretations. Agricultural and Forest Meteorology 87 (1997) 291-300\nNielsen, D. C., & Hinkle, S. E. (1996). Field evaluation of basal crop coefficients for corn based on growing degree days, growth stage, or time. Transactions of the ASAE, 39(1), 97-103.\nZhou, G. and Wang, Q., 2018. A new nonlinear method for calculating growing degree days. Scientific reports, 8(1), pp.1-14." + }, + { + "objectID": "exercises/evapotranspiration.html#define-auxiliary-functions", + "href": "exercises/evapotranspiration.html#define-auxiliary-functions", + "title": "52  Reference evapotranspiration", + "section": "Define auxiliary functions", + "text": "Define auxiliary functions\nTHe following functions to compute the saturation vapor pressure and the extraterrestrial solar radiation appear in more than one model, so to avoid repeating code, we will define a function for each of them.\n\ndef compute_esat(T):\n \"\"\"Function that computes saturation vapor pressure based Tetens formula\"\"\"\n e_sat = 0.6108 * np.exp(17.27 * T/(T+237.3)) \n return e_sat\n\ndef compute_Ra(doy, latitude):\n \"\"\"Function that computes extra-terrestrial solar radiation\"\"\"\n dr = 1 + 0.033 * np.cos(2 * np.pi * doy/365) # Inverse relative distance Earth-Sun\n phi = np.pi / 180 * latitude # Latitude in radians\n d = 0.409 * np.sin((2 * np.pi * doy/365) - 1.39) # Solar delcination\n omega = np.arccos(-np.tan(phi) * np.tan(d)) # Sunset hour angle\n Gsc = 0.0820 # Solar constant\n Ra = 24 * 60 / np.pi * Gsc * dr * (omega * np.sin(phi) * np.sin(d) + np.cos(phi) * np.cos(d) * np.sin(omega))\n return Ra" + }, + { + "objectID": "exercises/evapotranspiration.html#dalton-model", + "href": "exercises/evapotranspiration.html#dalton-model", + "title": "52  Reference evapotranspiration", + "section": "Dalton model", + "text": "Dalton model\nIn 1802, John Dalton proposed a model for predicting open-water evaporation, considering wind speed and vapor pressure deficit. While effective for open water bodies, the model doesn’t account for plant and soil effects. This model is classified as a mass-transfer model, which describes water vapor moving along a gradient, which is maintained or enhanced by wind. This wind action replaces the moisture-saturated air near the evaporation surface with drier air, effectively sustaining the vapor gradient\n E = u(e_{s} - e_{a})\nu is the wind speed in m/s e_s is the atmospheric saturation vapor pressure in kPa e_a is the actual atmospheric vapor pressure\n\n## John Dalton (1802)\n\ndef dalton(T_min,T_max,RH_min,RH_max,wind_speed):\n \"\"\"Potential evaporation model proposed by Dalton in 1802\"\"\"\n e_sat_min = compute_esat(T_min)\n e_sat_max = compute_esat(T_max)\n e_sat = (e_sat_min + e_sat_max)/2\n e_atm = (e_sat_min*(RH_max/100) + e_sat_max*(RH_min/100))/ 2\n PE = (3.648 + 0.7223*wind_speed)*(e_sat - e_atm)\n return PE" + }, + { + "objectID": "exercises/evapotranspiration.html#penman-model", + "href": "exercises/evapotranspiration.html#penman-model", + "title": "52  Reference evapotranspiration", + "section": "Penman model", + "text": "Penman model\n\n## Penman (1948)\n\ndef penman(T_min,T_max,RH_min,RH_max,wind_speed):\n \"\"\"Potential evapotranspiration model proposed by Penman in 1948\"\"\"\n e_sat_min = compute_esat(T_min)\n e_sat_max = compute_esat(T_max)\n e_sat = (e_sat_min + e_sat_max)/2\n e_atm = (e_sat_min*(RH_max/100) + e_sat_max*(RH_min/100))/ 2\n PET = (2.625 + 0.000479/wind_speed)*(e_sat - e_atm)\n return PET" + }, + { + "objectID": "exercises/evapotranspiration.html#romanenko-model", + "href": "exercises/evapotranspiration.html#romanenko-model", + "title": "52  Reference evapotranspiration", + "section": "Romanenko model", + "text": "Romanenko model\n\n## Romanenko (1961)\n\ndef romanenko(T_min,T_max,RH_min,RH_max):\n \"\"\"Potential evaporation model proposed by Romanenko in 1961\"\"\"\n T_avg = (T_min + T_max)/2\n RH_avg = (RH_min + RH_max)/2\n PET = 0.00006*(25 + T_avg)**2*(100 - RH_avg)\n return PET" + }, + { + "objectID": "exercises/evapotranspiration.html#jensen-haise-model", + "href": "exercises/evapotranspiration.html#jensen-haise-model", + "title": "52  Reference evapotranspiration", + "section": "Jensen-Haise model", + "text": "Jensen-Haise model\n\n## Jensen-Haise (1963)\n\ndef jensen_haise(T_min,T_max,doy,latitude):\n \"\"\"Potential evapotranspiration model proposed by Jensen in 1963\"\"\"\n Ra = compute_Ra(doy, latitude)\n T_avg = (T_min + T_max)/2\n PET = 0.0102 * (T_avg+3) * Ra\n return PET" + }, + { + "objectID": "exercises/evapotranspiration.html#hargreaves-model", + "href": "exercises/evapotranspiration.html#hargreaves-model", + "title": "52  Reference evapotranspiration", + "section": "Hargreaves model", + "text": "Hargreaves model\n PET = 0.0023 \\ R_a \\ (T_{avg} + 17.8) \\ \\sqrt{ (T_{max} - T_{min}) }\nR_a is the extraterrestrial solar radiation (MJ/m^2) T_{max} is the maximum daily air temperature T_{min} is the minimum daily air temperature T_{avg} is the average daily air temperature\n\n## Hargreaves (1982)\n\ndef hargreaves(T_min,T_max,doy,latitude):\n \"\"\"Potential evapotranspiration model proposed by Hargreaves in 1982\"\"\"\n Ra = compute_Ra(doy, latitude)\n T_avg = (T_min + T_max)/2\n PET = 0.0023 * Ra * (T_avg + 17.8) * (T_max - T_min)**0.5\n return PET" + }, + { + "objectID": "exercises/evapotranspiration.html#penman-monteith-model", + "href": "exercises/evapotranspiration.html#penman-monteith-model", + "title": "52  Reference evapotranspiration", + "section": "Penman-Monteith model", + "text": "Penman-Monteith model\n\ndef penman_monteith(T_min,T_max,RH_min,RH_max,solar_rad,wind_speed,doy,latitude,altitude):\n T_avg = (T_min + T_max)/2\n atm_pressure = 101.3 * ((293 - 0.0065 * altitude) / 293)**5.26 # Can be also obtained from weather station\n Cp = 0.001013; # Approx. 0.001013 for average atmospheric conditions\n epsilon = 0.622\n Lambda = 2.45\n gamma = (Cp * atm_pressure) / (epsilon * Lambda) # Approx. 0.000665\n\n ##### Wind speed\n wind_height = 1.5 # Most common height in meters\n wind_speed_2m = wind_speed * (4.87 / np.log((67.8 * wind_height) - 5.42)) # Eq. 47, FAO-56 wind height in [m]\n\n ##### Air humidity and vapor pressure\n delta = 4098 * (0.6108 * np.exp(17.27 * T_avg / (T_avg + 237.3))) / (T_avg + 237.3)**2\n e_temp_max = 0.6108 * np.exp(17.27 * T_max / (T_max + 237.3)) # Eq. 11, //FAO-56\n e_temp_min = 0.6108 * np.exp(17.27 * T_min / (T_min + 237.3))\n e_saturation = (e_temp_max + e_temp_min) / 2\n e_actual = (e_temp_min * (RH_max / 100) + e_temp_max * (RH_min / 100)) / 2\n\n ##### Solar radiation\n \n # Extra-terrestrial solar radiation\n dr = 1 + 0.033 * np.cos(2 * np.pi * doy/365) # Eq. 23, FAO-56\n phi = np.pi / 180 * latitude # Eq. 22, FAO-56\n d = 0.409 * np.sin((2 * np.pi * doy/365) - 1.39)\n omega = np.arccos(-np.tan(phi) * np.tan(d))\n Gsc = 0.0820 # Approx. 0.0820\n Ra = 24 * 60 / np.pi * Gsc * dr * (omega * np.sin(phi) * np.sin(d) + np.cos(phi) * np.cos(d) * np.sin(omega))\n\n # Clear Sky Radiation: Rso (MJ/m2/day)\n Rso = (0.75 + (2 * 10**-5) * altitude) * Ra # Eq. 37, FAO-56\n\n # Rs/Rso = relative shortwave radiation (limited to <= 1.0)\n alpha = 0.23 # 0.23 for hypothetical grass reference crop\n Rns = (1 - alpha) * solar_rad # Eq. 38, FAO-56\n sigma = 4.903 * 10**-9\n maxTempK = T_max + 273.16\n minTempK = T_min + 273.16\n Rnl = sigma * (maxTempK**4 + minTempK**4) / 2 * (0.34 - 0.14 * np.sqrt(e_actual)) * (1.35 * (solar_rad / Rso) - 0.35) # Eq. 39, FAO-56\n Rn = Rns - Rnl # Eq. 40, FAO-56\n\n # Soil heat flux density\n soil_heat_flux = 0 # Eq. 42, FAO-56 G = 0 for daily time steps [MJ/m2/day]\n\n # ETo calculation\n PET = (0.408 * delta * (solar_rad - soil_heat_flux) + gamma * (900 / (T_avg + 273)) * wind_speed_2m * (e_saturation - e_actual)) / (delta + gamma * (1 + 0.34 * wind_speed_2m))\n return np.round(PET,2)" + }, + { + "objectID": "exercises/evapotranspiration.html#data", + "href": "exercises/evapotranspiration.html#data", + "title": "52  Reference evapotranspiration", + "section": "Data", + "text": "Data\nIn this section we will use real data to test the different PET models.\n\n# Import data\ndf = pd.read_csv('../datasets/acme_ok_daily.csv')\ndf['Date'] = pd.to_datetime(df['Date'], format='%m/%d/%y %H:%M')\ndf.head()\n\n\n\n\n\n\n\n\nDate\nDOY\nTMAX\nTMIN\nRAIN\nHMAX\nHMIN\nATOT\nW2AVG\nETgrass\n\n\n\n\n0\n2005-01-01\n1\n21.161111\n14.272222\n0.00\n97.5\n65.97\n4.09\n5.194592\n1.976940\n\n\n1\n2005-01-02\n2\n21.261111\n4.794444\n0.00\n99.3\n77.37\n4.11\n3.428788\n1.302427\n\n\n2\n2005-01-03\n3\n5.855556\n3.477778\n2.54\n99.8\n98.20\n2.98\n3.249973\n0.349413\n\n\n3\n2005-01-04\n4\n4.644444\n0.883333\n7.62\n99.6\n98.50\n1.21\n3.527137\n0.288802\n\n\n4\n2005-01-05\n5\n0.827778\n-9.172222\n24.13\n99.4\n86.80\n1.65\nNaN\n0.367956\n\n\n\n\n\n\n\n\nlatitude = 34\naltitude = 350 # m\nPET_dalton = dalton(df['TMIN'], df['TMAX'], df['HMIN'], df['HMAX'], df['W2AVG'])\nPET_penman = penman(df['TMIN'], df['TMAX'], df['HMIN'], df['HMAX'], df['W2AVG'])\nPET_romanenko = romanenko(df['TMIN'], df['TMAX'], df['HMIN'], df['HMAX'])\nPET_jensen_haise = jensen_haise(df['TMIN'], df['TMAX'], df['DOY'], latitude)\nPET_hargreaves = hargreaves(df['TMIN'], df['TMAX'], df['DOY'], latitude)\nPET_penman_monteith = penman_monteith(df['TMIN'], df['TMAX'], df['HMIN'], df['HMAX'], df['ATOT'],df['W2AVG'],df['DOY'],latitude,altitude)\n\n\n# Plot models\nplt.figure(figsize=(10,4))\nplt.plot(df['Date'], PET_dalton, label='Dalton')\nplt.plot(df['Date'], PET_penman, label='Penman')\nplt.plot(df['Date'], PET_romanenko, label='Romanenko')\nplt.plot(df['Date'], PET_jensen_haise, label='Jense-Haise')\nplt.plot(df['Date'], PET_hargreaves, label='Hargreaves')\nplt.plot(df['Date'], PET_penman_monteith, label='Penman-Monteith')\nplt.ylabel('Evapotranspiration (mm/day)')\nplt.legend()\nplt.show()\n\n\n\n\n\n# Compare all models\ndf_models = pd.DataFrame({'date':df['Date'],'dalton':PET_dalton, 'penman':PET_penman, 'romanenko':PET_romanenko,\n 'jensen-haise':PET_jensen_haise, 'hargreaves':PET_hargreaves,\n 'penman_monteith':PET_penman_monteith})\n\n\n# Compare all models using a pairplot figure\n\nsns.pairplot(df_models, corner=True)\nsns.set(font_scale=2)" + }, + { + "objectID": "exercises/evapotranspiration.html#practice", + "href": "exercises/evapotranspiration.html#practice", + "title": "52  Reference evapotranspiration", + "section": "Practice", + "text": "Practice\n\nCalculate the mean absolute difference of all models against the Penman-Monteith model. What are the parsimonious models that best agree with the Penman-Monteith model? In what situations you may consider some of the simpler models?\nUsing the Penman-Monteith model, what is the impact of wind speed? For instance, what is the impact on ETo when wind speed is increased by 1 m/s and maintaining all the other variables constant?" + }, + { + "objectID": "exercises/evapotranspiration.html#references", + "href": "exercises/evapotranspiration.html#references", + "title": "52  Reference evapotranspiration", + "section": "References", + "text": "References\nDalton J (1802) Experimental essays on the constitution of mixed gases; on the force of steam of vapour from waters and other liquids in different temperatures, both in a Torricellian vacuum and in air on evaporation and on the expansion of gases by heat. Mem Manch Lit Philos Soc 5:535–602\nHargreaves G (1989) Preciseness of estimated potential evapotranspiration. J Irrig Drain Eng 115(6):1000–1007\nPenman HC (1948) Natural evaporation from open water, bare soil and grass. Proc R Soc Lond Ser A 193:120–145\nThornthwaite, C.W., 1948. An approach toward a rational classification of climate. Geographical review, 38(1), pp.55-94.\nMcMahon, T.A., Finlayson, B.L. and Peel, M.C., 2016. Historical developments of models for estimating evaporation using standard meteorological data. Wiley Interdisciplinary Reviews: Water, 3(6), pp.788-818." + }, + { + "objectID": "exercises/wheat_potential_yield.html#single-growing-season", + "href": "exercises/wheat_potential_yield.html#single-growing-season", + "title": "54  Modeling wheat yield potential", + "section": "Single growing season", + "text": "Single growing season\nFor simulating a single crop growing season the easiest approach is to select the rows of the historic weather record matching the specified growing season and then iterating over each day. More advanced methods could use a while loop to check if a condition, like the number of GDD, have been met to terminate the growing season.\n\n# Select records for growing season\nidx_growing_season = (df[\"DATES\"] >= planting_date) & (df[\"DATES\"] <= harvest_date)\ndf_season = df.loc[idx_growing_season,:].reset_index(drop=True)\ndf_season.head(3)\n\n\n\n\n\n\n\n\nWBANNO\nLST_DATE\nDATES\nCRX_VN\nLONGITUDE\nLATITUDE\nT_DAILY_MAX\nT_DAILY_MIN\nT_DAILY_MEAN\nT_DAILY_AVG\n...\nSOIL_MOISTURE_5_DAILY\nSOIL_MOISTURE_10_DAILY\nSOIL_MOISTURE_20_DAILY\nSOIL_MOISTURE_50_DAILY\nSOIL_MOISTURE_100_DAILY\nSOIL_TEMP_5_DAILY\nSOIL_TEMP_10_DAILY\nSOIL_TEMP_20_DAILY\nSOIL_TEMP_50_DAILY\nSOIL_TEMP_100_DAILY\n\n\n\n\n0\n53974\n20071001\n2007-10-01\n1.302\n-96.61\n39.1\n28.2\n7.1\n17.6\n18.9\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n1\n53974\n20071002\n2007-10-02\n1.302\n-96.61\n39.1\n28.2\n9.3\n18.7\n21.0\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n2\n53974\n20071003\n2007-10-03\n1.302\n-96.61\n39.1\n26.6\n5.9\n16.2\n16.3\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n\n\n3 rows × 29 columns\n\n\n\n\n# Initial conditions\nGDD = calculate_GDD(df_season[\"T_DAILY_AVG\"].iloc[0], Tbase)\nLAI = np.array([0])\nB = np.array([0])\n\n# Iterate over each day\n# We start from day 1, since we depend on information of the previous day\nfor k,t in enumerate(range(1,df_season.shape[0])):\n\n # Compute growing degree days\n GDD_day = calculate_GDD(df_season[\"T_DAILY_AVG\"].iloc[t], Tbase)\n GDD = np.append(GDD, GDD_day)\n\n # Compute leaf area index\n LAI_day = calculate_LAI(GDD.sum())\n LAI = np.append(LAI, LAI_day)\n\n # Estimate PAR from solar radiation (about 48%)\n PAR_day = df_season[\"SOLARAD_DAILY\"].iloc[t] * 0.48\n \n # Compute daily biomass\n B_day = calculate_B(LAI[t], PAR_day)\n\n # Compute cumulative biomass\n B = np.append(B, B[-1] + B_day)\n\n\n# Add variable to growing season dataframe, \n# so that we have everything in one place.\ndf_season['LAI'] = LAI\ndf_season['GDD'] = GDD\ndf_season['B'] = B\n\ndf_season.head(3)\n\n\n\n\n\n\n\n\nWBANNO\nLST_DATE\nDATES\nCRX_VN\nLONGITUDE\nLATITUDE\nT_DAILY_MAX\nT_DAILY_MIN\nT_DAILY_MEAN\nT_DAILY_AVG\n...\nSOIL_MOISTURE_50_DAILY\nSOIL_MOISTURE_100_DAILY\nSOIL_TEMP_5_DAILY\nSOIL_TEMP_10_DAILY\nSOIL_TEMP_20_DAILY\nSOIL_TEMP_50_DAILY\nSOIL_TEMP_100_DAILY\nLAI\nGDD\nB\n\n\n\n\n0\n53974\n20071001\n2007-10-01\n1.302\n-96.61\n39.1\n28.2\n7.1\n17.6\n18.9\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n0.000000\n14.9\n0.000000\n\n\n1\n53974\n20071002\n2007-10-02\n1.302\n-96.61\n39.1\n28.2\n9.3\n18.7\n21.0\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n0.008062\n17.0\n0.065197\n\n\n2\n53974\n20071003\n2007-10-03\n1.302\n-96.61\n39.1\n26.6\n5.9\n16.2\n16.3\n...\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\nNaN\n0.011653\n12.3\n0.198516\n\n\n\n\n3 rows × 32 columns\n\n\n\n\n# Generate figure for growing season LAI and Biomass\nplt.figure(figsize=(10,8))\n\n# Leaf area index\nplt.subplot(2,1,1)\nplt.plot(df_season['DATES'], df_season['LAI'], '-g')\nplt.ylabel('Leaf Area Index', color='g', size=16)\n\n# Biomass\nplt.twinx()\nplt.plot(df_season['DATES'], df_season['B'], '--b')\nplt.ylabel('Biomass ($g/m^2$)', color='b', size=16)\n\nplt.show()\n\n\n\n\n\n# Estimate grain yield\nY = df_season['B'].iloc[-1]*HI # grain yield in g/m^2\nY = Y * 10_000/1000 # Convert to kg per hectare (1 ha = 10,000 m^2) and (1 kg = 1,000 g)\nprint(f'Wheat yield potential is: {Y:.1f} kg per hectare')\n\nWheat yield potential is: 6090.5 kg per hectare" + }, + { + "objectID": "exercises/wheat_potential_yield.html#practice", + "href": "exercises/wheat_potential_yield.html#practice", + "title": "54  Modeling wheat yield potential", + "section": "Practice", + "text": "Practice\n\nModify the code so that the model stops when the crop accumulated a total of 2400 GDD. Hint: You no longer a harvest date and you may want to consider using a while loop." + }, + { + "objectID": "exercises/wheat_potential_yield.html#references", + "href": "exercises/wheat_potential_yield.html#references", + "title": "54  Modeling wheat yield potential", + "section": "References", + "text": "References\nBrun, F., Wallach, D., Makowski, D. and Jones, J.W., 2006. Working with dynamic crop models: evaluation, analysis, parameterization, and applications. Elsevier." + }, + { + "objectID": "exercises/autoregressive_model.html#read-and-explore-dataset", + "href": "exercises/autoregressive_model.html#read-and-explore-dataset", + "title": "55  Autogregressive model", + "section": "Read and explore dataset", + "text": "Read and explore dataset\n\n# Define column names\ncol_names = ['year','month','decimal_date','avg_co2',\n 'de_seasonalized','days','std','uncertainty']\n\n# Read dataset with custom column names\ndf = pd.read_csv('../datasets/co2_mm_mlo.txt', comment='#', delimiter='\\s+', names=col_names)\n\n# Display a few rows\ndf.head(3)\n\n\n\n\n\n\n\n\nyear\nmonth\ndecimal_date\navg_co2\nde_seasonalized\ndays\nstd\nuncertainty\n\n\n\n\n0\n1958\n3\n1958.2027\n315.70\n314.43\n-1\n-9.99\n-0.99\n\n\n1\n1958\n4\n1958.2877\n317.45\n315.16\n-1\n-9.99\n-0.99\n\n\n2\n1958\n5\n1958.3699\n317.51\n314.71\n-1\n-9.99\n-0.99\n\n\n\n\n\n\n\n\n# Add date column\ndf['date'] = pd.to_datetime({'year':df['year'],\n 'month':df['month'],\n 'day':1})\n\n# Set timestamp as index (specify the freq for the statsmodels package)\ndf.set_index('date', inplace=True)\ndf.index.freq = 'MS' # print(df.index.freq) to check that is not None\ndf.head(3)\n\n\n\n\n\n\n\n\nyear\nmonth\ndecimal_date\navg_co2\nde_seasonalized\ndays\nstd\nuncertainty\n\n\ndate\n\n\n\n\n\n\n\n\n\n\n\n\n1958-03-01\n1958\n3\n1958.2027\n315.70\n314.43\n-1\n-9.99\n-0.99\n\n\n1958-04-01\n1958\n4\n1958.2877\n317.45\n315.16\n-1\n-9.99\n-0.99\n\n\n1958-05-01\n1958\n5\n1958.3699\n317.51\n314.71\n-1\n-9.99\n-0.99\n\n\n\n\n\n\n\n\n# Check if we have any missing values\ndf.isna().sum()\n\nyear 0\nmonth 0\ndecimal_date 0\navg_co2 0\nde_seasonalized 0\ndays 0\nstd 0\nuncertainty 0\ndtype: int64\n\n\n\n# Visualize time series data\nplt.figure(figsize=(6,4))\nplt.plot(df['avg_co2'])\nplt.ylabel('$CO_2$ (ppm)')\nplt.show()" + }, + { + "objectID": "exercises/autoregressive_model.html#test-for-stationarity", + "href": "exercises/autoregressive_model.html#test-for-stationarity", + "title": "55  Autogregressive model", + "section": "Test for stationarity", + "text": "Test for stationarity\nStationarity in a time series implies that the statistical properties of the series like mean, variance, and autocorrelation are constant over time. In a stationary time series, these properties do not depend on the time at which the series is observed, meaning that the series does not exhibit trends or seasonal effects. Non-stationary data typically show clear trends, cyclical patterns, or other systematic changes over time. Non-stationary time series often need to be transformed (or de-trended) to become stationary before analysis.\nThe Dickey-Fuller (adfuller) test provided by the statsmodels library can be helpful to statistically test for stationarity.\nDickey-Fuller test - Null Hypothesis: The series is NOT stationary - Alternate Hypothesis: The series is stationary.\nThe null hypothesis can be rejected if p-value<0.05. Hence, if the p-value is >0.05, the series is non-stationary.\n\n# Dickey-Fuller test\nresults = adfuller(df['avg_co2'])\nprint(f\"p-value is {results[1]}\")\n\np-value is 1.0" + }, + { + "objectID": "exercises/autoregressive_model.html#create-training-and-testing-sets", + "href": "exercises/autoregressive_model.html#create-training-and-testing-sets", + "title": "55  Autogregressive model", + "section": "Create training and testing sets", + "text": "Create training and testing sets\nLet’s use 95% of the dataset to fit the model and the remaining 5%, more recent, observations to test our forecast.\n\n# Train and test sets\nidx_train = df['year'] < 2017\ndf_train = df[idx_train]\ndf_test = df[~idx_train]" + }, + { + "objectID": "exercises/autoregressive_model.html#decompose-time-series", + "href": "exercises/autoregressive_model.html#decompose-time-series", + "title": "55  Autogregressive model", + "section": "Decompose time series", + "text": "Decompose time series\n\n# Decompose time series\n# Extrapolate to avoid NaNs\nresults = seasonal_decompose(df_train['avg_co2'], \n model='additive', \n period=12,\n extrapolate_trend='freq')\n \n\n\n# Create figure with trend components\nplt.figure(figsize=(6,6))\n \nplt.subplot(3,1,1)\nplt.title('Trend')\nplt.plot(results.trend, label='Trend')\nplt.ylabel('$CO_2$ (ppm)')\n \nplt.subplot(3,1,2)\nplt.title('Seasonality')\nplt.plot(results.seasonal, label='Seasonal')\nplt.ylabel('$CO_2$ (ppm)')\n\nplt.subplot(3,1,3)\nplt.title('Residuals')\nplt.plot(results.resid, label='Residuals')\nplt.ylabel('$CO_2$ (ppm)')\n\nplt.subplots_adjust(hspace=0.5)\nplt.show()" + }, + { + "objectID": "exercises/autoregressive_model.html#examine-autocorrelation-lags", + "href": "exercises/autoregressive_model.html#examine-autocorrelation-lags", + "title": "55  Autogregressive model", + "section": "Examine autocorrelation lags", + "text": "Examine autocorrelation lags\nThe statsmodels module offers an extensive library of functions for time series analysis. In addition to autocorrelation function, we can also apply a partial autocorrelation function, that removes the effect of intermediate lags. For instance, the PACF between time t and time t-4 is the pure autocorrelation without the effect of t-1, t-2, and t-3. Autocorrelation plots will help us define the number of lags that we need to consider in our autoregressive model.\n\n# Create figure\nfig, ax = plt.subplots(figsize=(8,5), ncols=1, nrows=2)\n\n# Plot the autocorrelation function\nplot_acf(df['avg_co2'], ax=ax[0])\nax[0].set_xlabel('Lag (days)')\nax[0].set_ylabel('Correlation coefficient')\n\n# Plot the partial autocorrelation function\nplot_pacf(df['avg_co2'], ax[1], method='ywm')\nax[1].set_xlabel('Lag (days)')\nax[1].set_ylabel('Correlation coefficient')\n\nfig.subplots_adjust(hspace=0.5)\nplt.show()\n\n\n\n\n\n# Fit model to train set\nmodel = ARIMA(df_train['avg_co2'],\n order=(1,0,0), \n seasonal_order=(1,0,0,12),\n dates=df_train.index,\n trend=[1,1,1]\n ).fit()\n\n# (p,d,q) => autoregressive, differences, and moving average\n# (p,d,q,s) => autoregressive, differences, moving average, and periodicity\n# seasonal_order (3,0,0,12) means that we add 12, 24, and 36 month lags\n\n\n\n\n\n\n\nNote\n\n\n\nA trend with a constant, linear, and quadratic terms (trend=[1,1,1]) is probably enough to capture the short-term trend of the time series. This option is readily available within the ARIMA function and is probably enough to create a short-term forecast.\n\n\n\n# Mean absolute error against the train set\nprint(f\"MAE = {model.mae} ppm\")\n\nMAE = 0.3244007587766971 ppm\n\n\n\n# Print summary statistics\nmodel.summary()\n\n\nSARIMAX Results\n\n\nDep. Variable:\navg_co2\nNo. Observations:\n706\n\n\nModel:\nARIMA(1, 0, 0)x(1, 0, 0, 12)\nLog Likelihood\n-820.999\n\n\nDate:\nThu, 01 Feb 2024\nAIC\n1653.998\n\n\nTime:\n10:47:03\nBIC\n1681.355\n\n\nSample:\n03-01-1958\nHQIC\n1664.569\n\n\n\n- 12-01-2016\n\n\n\n\nCovariance Type:\nopg\n\n\n\n\n\n\n\n\n\ncoef\nstd err\nz\nP>|z|\n[0.025\n0.975]\n\n\nconst\n314.2932\n60.601\n5.186\n0.000\n195.518\n433.069\n\n\nx1\n0.0663\n0.170\n0.389\n0.697\n-0.268\n0.400\n\n\nx2\n8.585e-05\n0.000\n0.497\n0.619\n-0.000\n0.000\n\n\nar.L1\n0.8454\n0.156\n5.413\n0.000\n0.539\n1.152\n\n\nar.S.L12\n0.9696\n0.019\n50.944\n0.000\n0.932\n1.007\n\n\nsigma2\n1.3816\n0.117\n11.799\n0.000\n1.152\n1.611\n\n\n\n\n\n\nLjung-Box (L1) (Q):\n35.92\nJarque-Bera (JB):\n3.15\n\n\nProb(Q):\n0.00\nProb(JB):\n0.21\n\n\nHeteroskedasticity (H):\n1.15\nSkew:\n0.14\n\n\nProb(H) (two-sided):\n0.29\nKurtosis:\n3.15\n\n\n\nWarnings:[1] Covariance matrix calculated using the outer product of gradients (complex-step).\n\n\n\n# Plot diagnostic charts\nfig, ax = plt.subplots(nrows=2, ncols=2, figsize=(8,6))\nmodel.plot_diagnostics(lags=14, fig=fig)\nfig.subplots_adjust(hspace=0.5, wspace=0.3)\nplt.show()" + }, + { + "objectID": "exercises/autoregressive_model.html#predict-with-autoregressive-model", + "href": "exercises/autoregressive_model.html#predict-with-autoregressive-model", + "title": "55  Autogregressive model", + "section": "Predict with autoregressive model", + "text": "Predict with autoregressive model\n\n# Predict values for the remaining 5% of the data\npred_values = model.predict(start=df_test.index[0], end=df_test.index[-2])\n\n# Create figure\nplt.figure(figsize=(8,3))\nplt.plot(pred_values, color='tomato', label='Predicted test set')\nplt.plot(df_test['avg_co2'], color='k', label='Observed test set')\nplt.ylabel('$CO_2$ concentration')\nplt.xticks(rotation=10)\nplt.legend()\nplt.show()\n\n\n\n\n\n# Mean absolute error against test set\nmae_predicted = np.mean(np.abs(df['avg_co2'] - pred_values))\nprint(f'MAE = {mae_predicted:.2f} ppm')\n\nMAE = 0.50 ppm" + }, + { + "objectID": "exercises/autoregressive_model.html#create-2030-forecast", + "href": "exercises/autoregressive_model.html#create-2030-forecast", + "title": "55  Autogregressive model", + "section": "Create 2030 forecast", + "text": "Create 2030 forecast\n\n# Forecast concentration until 2030\nforecast_values = model.predict(start=pd.to_datetime('2020-01-01'), \n end=pd.to_datetime('2030-06-01'))\n\n# Print concentration in 2030\nprint(f'Concentration in 2030 is expected to be: {forecast_values.iloc[-1]:.0f} ppm')\n\n# Create figure\nplt.figure(figsize=(8,3))\nplt.plot(forecast_values, color='tomato', label='Observed')\nplt.plot(df_test['avg_co2'], color='k', label='Forecast')\nplt.ylabel('$CO_2$ concentration')\nplt.legend()\nplt.show()\n\nConcentration in 2030 is expected to be: 439 ppm" + }, + { + "objectID": "exercises/basic_statistical_tests.html", + "href": "exercises/basic_statistical_tests.html", + "title": "56  Basic statistical tests", + "section": "", + "text": "57 Analysis of Variance\nThe goal of analysis of variance (ANOVA) is to assess whether there are statistically significant differences between the means of three or more groups. It helps to determine whether any observed differences in the group means are likely to be genuine or simply due to random variation.\nA one-way ANOVA is used to analyze the effect of a single categorical factor on a continuous variable.\nA two-way ANOVA is used to assess the influence of two categorical factors simultaneously on a continuous variable. It allows for examining both main effects of each factor as well as their interaction effect.\nIn this exercise we will use a dataset of corn yield for different treatments of nitrogen fertilizer on multiple US states. The dataset is a subset of the study published by Tremblay et al., 2012 (see references for more details) and it was obtained from (http://www.nue.okstate.edu/).\nimport pandas as pd\nfrom scipy import stats\nimport matplotlib.pyplot as plt\nfrom statsmodels.formula.api import ols\nfrom statsmodels.stats.anova import anova_lm\nfrom statsmodels.stats.multicomp import MultiComparison\n# Load data\ndf = pd.read_csv(\"../datasets/corn_nue_multiple_locs.csv\")\ndf.head()\n\n\n\n\n\n\n\n\nYear\nState\nSite\nTextural_class\nReplications\nTreatments\nN_Planting_kg_ha\nN_Sidedress_kg_ha\nN_Total_kg_ha\nYield_T_ha\n\n\n\n\n0\n2006\nIllinois\nPad\nSilt loam\n1\n1\n0\n0\n0\n3.26\n\n\n1\n2006\nIllinois\nPad\nSilt loam\n1\n3\n36\n0\n36\n4.15\n\n\n2\n2006\nIllinois\nPad\nSilt loam\n1\n5\n36\n54\n90\n8.64\n\n\n3\n2006\nIllinois\nPad\nSilt loam\n1\n7\n36\n107\n143\n10.52\n\n\n4\n2006\nIllinois\nPad\nSilt loam\n1\n9\n36\n161\n197\n11.47\n# Print some useful properties of the dataset\nprint(df['Site'].unique()) # Locations\nprint(df['Treatments'].unique()) # Treatments\nprint(df['Replications'].unique()) # Replications\nprint(df.shape)\n\n\n['Pad' 'Dixon' 'Manhattan' 'Copeland' 'Diederick']\n[1 3 5 7 9]\n[1 2 3 4]\n(120, 10)\n# Examine yield data using boxplots for all locations combined\ndf.boxplot(figsize=(5,5), column='Yield_T_ha', by='N_Total_kg_ha')\nplt.show()\n# Examine yield by state\ndf.boxplot(figsize=(4,5), column='Yield_T_ha', by='State')\nplt.show()\n# Examine yield by site\ndf.boxplot(figsize=(4,5), column='Yield_T_ha', by='Site')\nplt.show()" + }, + { + "objectID": "exercises/basic_statistical_tests.html#power-analysis", + "href": "exercises/basic_statistical_tests.html#power-analysis", + "title": "56  Basic statistical tests", + "section": "Power analysis", + "text": "Power analysis\n\n# Import modules\nimport numpy as np\n\nA power analysis is an important concept in experimental design and hypothesis testing, as it helps researchers determine the appropriate sample size needed to detect a significant effect, if it exists. Without adequate power, studies may fail to detect genuine effects, leading to inconclusive or misleading results.\nIn this context, pilot experiments are essential to quantify the approximate variability of a response variable. In the example below our goal is to identify how many downward-facing images we need to collect in a field to compute the percentage of green canopy cover withing a 5% with a 95% confidence. The provided values are the result of an exploratory field data collection to compute the variability of the field. The power analysis rests on the notion that:\n n = \\Bigg( \\frac{1.96 \\sigma}{\\delta} \\Bigg)^2 \nwhere n is the number of samples to be collected, \\delta is the error marging with 95% confidence, 1.96 is the z-score, and \\sigma is the standard deviation (required in advance). For more insights, check the manuscript by Patrignani and Ochsner, 2015.\n\n# Values from a pilot experiment\ncanopy_cover = np.array([25, 16, 19, 23, 12]) # Percent\n\n\n# Define parameters\nz_score = 1.96 # z-score for a 95% confidence level\nmargin_of_error = 5 # Margin of error\nstandard_deviation = np.std(canopy_cover)\n\n# Calculate sample size\nsample_size = (z_score * standard_deviation / margin_of_error) ** 2\n\nprint(\"Sample size needed:\", round(sample_size))\n\nSample size needed: 3" + }, + { + "objectID": "exercises/basic_statistical_tests.html#one-sample-t-test", + "href": "exercises/basic_statistical_tests.html#one-sample-t-test", + "title": "56  Basic statistical tests", + "section": "One-sample T-test", + "text": "One-sample T-test\nThe scipy.stats module includes function like ttest_1samp that enables researchers to determine whether the mean of a sample significantly differs from a known population mean. For instance, suppose a farmer wants to assess whether a new wheat variety significantly increases the average yield of a specific crop compared to the historical average yield. By collecting yield data from different wheat variety and using ttest_1samp, the farmer can infer whether the observed increase in yield by the fields with the new variety is statistically significant.\nQuestion: Do newer wheat varieties have a statistically significant performance compared to the existing varieties?\n\n# Import modules\nimport pandas as pd\nfrom scipy import stats\n\n\n# Read dataset\ndf_new_varieties = pd.read_csv('../datasets/wheat_variety_trial_2023_greeley_county.csv', skiprows=[0])\ndf_new_varieties.head(3)\n\n\n\n\n\n\n\n\nbrand\nname\nyield_bu_ac\n\n\n\n\n0\nLIMAGRAIN\nLCH19DH-152-6\n50.7\n\n\n1\nPOLANSKY\nROCKSTAR\n45.2\n\n\n2\nKWA\nKS WESTERN STAR\n44.5\n\n\n\n\n\n\n\n\n# Define historical average yield of farm\nhistorical_mean_yield = 8 # bu/ac\n\n\n# Estimate mean of all new varieties\ndf_new_varieties['yield_bu_ac'].mean().round()\n\n34.0\n\n\n\nTwo-sided test\nNull hypothesis: the mean yield of the new varieties is the same as the historical field yield (popmean).\nAlternative hypothesis: the mean yield of the new varieties is different than the historical field yield (popmean).\n\n# Define historical average yield of farm\n# Perform a one-sample t-test for each new variety against the historical data\nresults_two_sided = stats.ttest_1samp(df_new_varieties['yield_bu_ac'],\n historical_mean_yield,\n alternative='two-sided')\n\nprint(f\"t_statistic: {results_two_sided[0]}\")\nprint(f\"p_value: {results_two_sided[1]}\")\n\nt_statistic: 22.10361576051108\np_value: 5.9931027593421575e-21\n\n\n\n# Define significance level\nalpha = 0.05\n\n# Find whether we accept or reject the null hypothesis\nif results_two_sided[1] < alpha:\n print(\"\"\"We reject the null hypothesis in favor of the alternative hypothesis that \n the mean yield of new varieties is statistically different from the historical yield.\"\"\")\nelse:\n print(\"\"\"We accept the null hypothesis. There is no statistically significant evidence that the combined \n mean yield of new varieties differs from the historical yield.\"\"\")\n\n\nWe reject the null hypothesis in favor of the alternative hypothesis that \n the mean yield of new varieties is statistically different from the historical yield.\n\n\n\n\nTwo-sided confidence intervals\n\n# Compute 95% confidence intervals\nci = results_two_sided.confidence_interval(confidence_level=0.95)\nprint(ci)\n\nConfidenceInterval(low=31.6727760618944, high=36.47873908962076)\n\n\n\n\nOne-sided test (less option)\nNull hypothesis: The mean of all new varieties is not less than the historical yield.\nAlternative hypothesis: The mean of all new varieties is less than the historical yield.\n\nresults_one_sided = stats.ttest_1samp(df_new_varieties['yield_bu_ac'], \n popmean=historical_mean_yield,\n alternative='less')\n\nprint(results_one_sided)\n\nTtestResult(statistic=22.10361576051108, pvalue=1.0, df=32)\n\n\n\nif results_one_sided[1] < alpha:\n print(\"\"\"We reject the null hypothesisin favor of the alternative hypothesis that \n the mean of the new varieties is less than the historical yield\"\"\")\nelse:\n print(\"\"\"We accept the null hypothesis that the mean of the new varieties \n is not less than the historical yield\"\"\")\n\n\nWe accept the null hypothesis that the mean of the new varieties \n is not less than the historical yield\n\n\n\n\nOne-sided test (greater)\nNull hypothesis: The mean of all new varieties is not greater than the historical yield.\nAlternative hypothesis: The mean of all new varieties is greater than the historical yield.\n\nresults_one_sided = stats.ttest_1samp(df_new_varieties['yield_bu_ac'], \n popmean=historical_mean_yield,\n alternative='greater')\n\nprint(results_one_sided)\n\nTtestResult(statistic=22.10361576051108, pvalue=2.9965513796710787e-21, df=32)\n\n\n\nif results_one_sided[1] < alpha:\n print(\"\"\"We reject the null hypothesis in favor of the alternative hypothesis that\n the mean of the new varieties is greater than the historical yield\"\"\")\nelse:\n print(\"\"\"We accept the null hypothesis that the mean of the new varieties \n is not greater than the historical yield\"\"\")\n\n\nWe reject the null hypothesis in favor of the alternative hypothesis that\n the mean of the new varieties is greater than the historical yield" + }, + { + "objectID": "exercises/basic_statistical_tests.html#two-sample-t-test", + "href": "exercises/basic_statistical_tests.html#two-sample-t-test", + "title": "56  Basic statistical tests", + "section": "Two-sample T-test", + "text": "Two-sample T-test\nThis analysis calculates the T-test for the means of two independent samples. In this exercise we will evaluate if the mean grain yield of two different corn hybrids are statistically different from each other.\nNull hypothesis: The mean yield of the two corn hybrids are not statistically different from each other.\nAlternative hypothesis: The means of the two corn hybrids are statistically different.\n\n# Import modules\nimport pandas as pd\nfrom scipy import stats\n\n\ndf = pd.read_csv('../datasets/corn_dryland_trial.csv', skiprows=[0])\ndf.head(3)\n\n\n\n\n\n\n\n\nbrand\nreplication\nyield_bu_ac\nlodging_perc\n\n\n\n\n0\nLEWIS\n1\n172.9\n47\n\n\n1\nLEWIS\n2\n218.3\n20\n\n\n2\nLEWIS\n3\n196.8\n40\n\n\n\n\n\n\n\n\n# Find mean for each brand\ndf.groupby(by='brand').mean()\n\n\n\n\n\n\n\n\nreplication\nyield_bu_ac\nlodging_perc\n\n\nbrand\n\n\n\n\n\n\n\nLEWIS\n3.0\n194.46\n43.4\n\n\nNK\n3.0\n170.40\n61.0\n\n\n\n\n\n\n\n\n# Get yield values for each group\nidx_group_1 = df['brand'] == 'LEWIS'\nvalues_group_1 = df.loc[idx_group_1,'yield_bu_ac']\n\nidx_group_2 = df['brand'] == 'NK'\nvalues_group_2 = df.loc[idx_group_2,'yield_bu_ac']\n\n\n# PRint statistical results\nt_statistic, p_value = stats.ttest_ind(values_group_1, values_group_2)\nprint(t_statistic, p_value)\n\n2.1525722332699373 0.06352024499469316\n\n\n\nalpha = 0.05\n\nif p_value < alpha:\n print(\"\"\"We reject the null hypothesis in favor of the alternative hypothesis that\n the mean of the two hybrids are statistically different from each other.\"\"\")\nelse:\n print(\"\"\"We accept the null hypothesis that the mean of the two hybrids\n are equal.\"\"\")\n\nWe accept the null hypothesis that the mean of the two hybrids\n are equal.\n\n\nWe can plot the data using a boxplot and inspect if the notches (which represent the 95% confidence interval of the median) overlap. If they do, then that suggests that the median values are not statistically different.\n\ndf.boxplot(figsize=(4,5), column='yield_bu_ac', by='brand', \n notch=True, showmeans=True, ylabel='Yield (bu/ac)');" + }, + { + "objectID": "exercises/basic_statistical_tests.html#anova-assumptions", + "href": "exercises/basic_statistical_tests.html#anova-assumptions", + "title": "56  Basic statistical tests", + "section": "ANOVA assumptions", + "text": "ANOVA assumptions\n\nSamples drawn from a population are normally distributed. Test: Shapiro-Wilk\nSamples drawn from all populations have (approximately) the same variance. This property is called homoscedasticity or homogeneity of variances.” Tests: Bartlett’s and Levene’s tests.\nSamples are independent of each other. Test: No test. Here we rely on the nature of the variable being observed and the experimental design.\n\n\n# Test the assumption of normality\n# Shapiro-WIlk's null hypothesis: Data was obtained from a normal distribution\nstats.shapiro(df['Yield_T_ha'])\n\nShapiroResult(statistic=0.9829135537147522, pvalue=0.1326330304145813)\n\n\n\n# Test for homogeneity of variance\n# Bartlett's null hypothesis: All the groups have equal variance\n\nD = {}\nfor tmt in df['Treatments'].unique():\n idx_tmt = df['Treatments'] == tmt\n D[tmt] = df.loc[idx_tmt, 'Yield_T_ha'].values\n \n#print(D)\nstats.bartlett(D[1], D[3], D[5], D[7], D[9])\n\nBartlettResult(statistic=6.054092711026625, pvalue=0.19514485256182393)" + }, + { + "objectID": "exercises/basic_statistical_tests.html#one-way-anova", + "href": "exercises/basic_statistical_tests.html#one-way-anova", + "title": "56  Basic statistical tests", + "section": "One-way ANOVA", + "text": "One-way ANOVA\nHere we will compare an independent variable with a single predictor. The predictor N_Total_kg_ha will be used as a categorical varaible. Alternatively we could use the Treatments column, but it is easier to read the table if we present the values using the actual treatment values, so that we quickly devise which Nitrogen rates show statistical differences.\nBelow we will explore the one-way ANOVA using both SciPy (simpler code) and Statsmodels (more complete output)\n\nUsing SciPy module\n\n# One-way test\nstats.f_oneway(D[1], D[3], D[5], D[7], D[9])\n\nF_onewayResult(statistic=11.431258827879908, pvalue=7.582810948341893e-08)\n\n\n\n# Tukey test\nprint(stats.tukey_hsd(D[1], D[3], D[5], D[7], D[9]))\n\nTukey's HSD Pairwise Group Comparisons (95.0% Confidence Interval)\nComparison Statistic p-value Lower CI Upper CI\n (0 - 1) -1.317 0.519 -3.642 1.007\n (0 - 2) -3.036 0.004 -5.361 -0.711\n (0 - 3) -4.310 0.000 -6.634 -1.985\n (0 - 4) -4.745 0.000 -7.070 -2.421\n (1 - 0) 1.317 0.519 -1.007 3.642\n (1 - 2) -1.718 0.250 -4.043 0.607\n (1 - 3) -2.992 0.005 -5.317 -0.667\n (1 - 4) -3.428 0.001 -5.753 -1.103\n (2 - 0) 3.036 0.004 0.711 5.361\n (2 - 1) 1.718 0.250 -0.607 4.043\n (2 - 3) -1.274 0.553 -3.599 1.051\n (2 - 4) -1.710 0.255 -4.034 0.615\n (3 - 0) 4.310 0.000 1.985 6.634\n (3 - 1) 2.992 0.005 0.667 5.317\n (3 - 2) 1.274 0.553 -1.051 3.599\n (3 - 4) -0.436 0.985 -2.761 1.889\n (4 - 0) 4.745 0.000 2.421 7.070\n (4 - 1) 3.428 0.001 1.103 5.753\n (4 - 2) 1.710 0.255 -0.615 4.034\n (4 - 3) 0.436 0.985 -1.889 2.761\n\n\n\n\n\nUsing Statsmodels module\n\n# Anova table with statsmodels\nformula = 'Yield_T_ha ~ C(N_Total_kg_ha)'\nanova_lm(ols(formula, data=df).fit())\n\n\n\n\n\n\n\n\n\ndf\nsum_sq\nmean_sq\nF\nPR(>F)\n\n\n\n\nC(N_Total_kg_ha)\n4.0\n386.085542\n96.521385\n11.431259\n7.582811e-08\n\n\nResidual\n115.0\n971.018108\n8.443636\nNaN\nNaN\n\n\n\n\n\n\n\nThe ANOVA table shows the there is significant differences between treatments. The catch is that we don’t know which groups are different. The ANOVA table only tells us that at least one group has a mean value that is substantially (read significantly) different from the rest.\nThe F is the F-statistic to test the null hypothesis that the corresponding coefficient is zero. The goal is to compare the mean variability within groups to the mean variability between groups. The F-statistic is just the ratio of the two (96.5/8.44=11.4). 96.52 is the variability between groups and 8.44 is the variability within the groups.\nThe pValue of the F-statistic indicates whether a factor is not significant at the 5% significance level given the other terms in the model.\nA mean multicomparison test can help us identify which treatments show signifcant differences.\n\n# Multicomparison test\ngroups = MultiComparison(df[\"Yield_T_ha\"],df['N_Total_kg_ha']).tukeyhsd(alpha=0.05)\nprint(groups)\n\nMultiple Comparison of Means - Tukey HSD, FWER=0.05\n===================================================\ngroup1 group2 meandiff p-adj lower upper reject\n---------------------------------------------------\n 0 36 1.3175 0.5192 -1.0073 3.6423 False\n 0 90 3.0358 0.004 0.711 5.3607 True\n 0 143 4.3096 0.0 1.9847 6.6344 True\n 0 197 4.7454 0.0 2.4206 7.0703 True\n 36 90 1.7183 0.2499 -0.6065 4.0432 False\n 36 143 2.9921 0.0047 0.6672 5.3169 True\n 36 197 3.4279 0.0008 1.1031 5.7528 True\n 90 143 1.2738 0.5527 -1.0511 3.5986 False\n 90 197 1.7096 0.2547 -0.6153 4.0344 False\n 143 197 0.4358 0.9852 -1.889 2.7607 False\n---------------------------------------------------\n\n\n\n# Visualize significantly different groups relative to a specific group\ngroups.plot_simultaneous(figsize=(5,4), comparison_name=90)\nplt.xlabel('Corn yield in T/ha', size=12)\nplt.ylabel('Nitrogen Treatments in kg/ha', size=12)\nplt.show()" + }, + { + "objectID": "exercises/basic_statistical_tests.html#two-way-anova", + "href": "exercises/basic_statistical_tests.html#two-way-anova", + "title": "56  Basic statistical tests", + "section": "Two-way ANOVA", + "text": "Two-way ANOVA\nIn this case we will add two predictors variables, soil textural class and total nitrogen applied. In many cases researchers add Location as a proxy for local environmental conditions (including soil) and Year as a proxy for the particular weather conditions during each growing season. In this case we have soil textural class available, so we will make use of that first, since it will give our results broader applications that, in principle, can be related to soil types elsewhere.\n\n# Two predictors\nformula = 'Yield_T_ha ~ C(N_Total_kg_ha) + C(Textural_class)'\nanova_lm(ols(formula, data=df).fit())\n\n\n\n\n\n\n\n\ndf\nsum_sq\nmean_sq\nF\nPR(>F)\n\n\n\n\nC(N_Total_kg_ha)\n4.0\n386.085542\n96.521385\n11.750059\n5.022955e-08\n\n\nC(Textural_class)\n1.0\n34.560000\n34.560000\n4.207172\n4.254546e-02\n\n\nResidual\n114.0\n936.458108\n8.214545\nNaN\nNaN\n\n\n\n\n\n\n\nSoil textual class was barely significant considering all locations and years at the the 0.05 level.\n\n# Two predictors with interaction\nformula = 'Yield_T_ha ~ C(N_Total_kg_ha) * C(Textural_class)'\nanova_lm(ols(formula, data=df).fit())\n\n\n\n\n\n\n\n\ndf\nsum_sq\nmean_sq\nF\nPR(>F)\n\n\n\n\nC(N_Total_kg_ha)\n4.0\n386.085542\n96.521385\n12.530231\n1.960683e-08\n\n\nC(Textural_class)\n1.0\n34.560000\n34.560000\n4.486516\n3.641510e-02\n\n\nC(N_Total_kg_ha):C(Textural_class)\n4.0\n89.119178\n22.279795\n2.892322\n2.547188e-02\n\n\nResidual\n110.0\n847.338930\n7.703081\nNaN\nNaN\n\n\n\n\n\n\n\nThe interaction of nitrogen rate and textural class also resulted statistically significant at the 0.05 level.\n\n# Classical ANOVA with Treatment, Location, and Year interactions\nformula = 'Yield_T_ha ~ C(N_Total_kg_ha) * State * C(Year)'\nanova_lm(ols(formula, data=df).fit())\n\n\n\n\n\n\n\n\ndf\nsum_sq\nmean_sq\nF\nPR(>F)\n\n\n\n\nC(N_Total_kg_ha)\n4.0\n386.085542\n96.521385\n20.522835\n3.270251e-12\n\n\nState\n2.0\n234.432785\n117.216393\n24.923106\n1.989675e-09\n\n\nC(Year)\n2.0\n112.504702\n56.252351\n11.960642\n2.328310e-05\n\n\nC(N_Total_kg_ha):State\n8.0\n140.688973\n17.586122\n3.739245\n7.649526e-04\n\n\nC(N_Total_kg_ha):C(Year)\n8.0\n36.595110\n4.574389\n0.972628\n4.621589e-01\n\n\nState:C(Year)\n4.0\n3.745613\n0.936403\n0.199103\n9.382617e-01\n\n\nC(N_Total_kg_ha):State:C(Year)\n16.0\n14.832950\n0.927059\n0.197116\n9.996634e-01\n\n\nResidual\n95.0\n446.796537\n4.703121\nNaN\nNaN\n\n\n\n\n\n\n\nIn a multi-treatment, multi-year, and multi-state study, it is not surprising that treatment, site, and year resulted highly significant (P<0.01).\nNote that the interactions N_Total_kg_ha:Year, State:Year, and N_Total_kg_ha:State:Year were not significant at the P<0.05 level." + }, + { + "objectID": "exercises/basic_statistical_tests.html#references", + "href": "exercises/basic_statistical_tests.html#references", + "title": "56  Basic statistical tests", + "section": "References", + "text": "References\nPatrignani, A., & Ochsner, T. E. (2015). Canopeo: A powerful new tool for measuring fractional green canopy cover. Agronomy journal, 107(6), 2312-2320.\nTremblay, N.,  Y.M. Bouroubi,  C. Bélec,  R.W. Mullen,  N.R. Kitchen,  W.E. Thomason,  S. Ebelhar,  D.B. Mengel,  W.R. Raun,  D.D. Francis,  E.D. Vories, and I. Ortiz-Monasterio. 2012. Corn response to nitrogen is influenced by soil texture and weather. Agron. J. 104:1658–1671. doi:10.2134/agronj2012.018." + }, + { + "objectID": "exercises/anscombe_quartet.html#practice", + "href": "exercises/anscombe_quartet.html#practice", + "title": "57  Anscombe’s quartet", + "section": "Practice", + "text": "Practice\n\nCompute the root mean square error, mean absolute error, and mean bias error for each dataset. Can any of these error metrics provide a better description of the goodness of fit for each dataset?\nInstead of creating the figure subplots one by one, can you write a for loop that will iterate over each dataset, fit a linear model, and then populate its correpsonding subplot?" + }, + { + "objectID": "exercises/anscombe_quartet.html#references", + "href": "exercises/anscombe_quartet.html#references", + "title": "57  Anscombe’s quartet", + "section": "References", + "text": "References\nAnscombe, F.J., 1973. Graphs in statistical analysis. The American Statistician, 27(1), pp.17-21." + }, + { + "objectID": "exercises/calibration_teros_12.html", + "href": "exercises/calibration_teros_12.html", + "title": "58  Calibration soil moisture sensor", + "section": "", + "text": "In this example we will use linear regression to develop a calibration equation for a soil moisture sensor. The raw sensor output consists of a voltage differential that needs to be correlated with volumetric water content in order to make soil moisture estimations.\nA laboratory calibration was conducted using containers with packed soil having a known soil moisture that we will use to develop the calibration curve. For each container and soil type we obtained voltage readings with the sensor and then we oven-dried to soil to find the true volumetric water content.\nIndependent variable: Sensor raw voltage readings (milliVolts)\nDependent variable: Volumetric water content (cm^3/cm^3)\n\n# Import modules\nimport numpy as np\nfrom numpy.polynomial import polynomial as P\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import r2_score\n\n\n# Read dataset\ndf = pd.read_csv('../datasets/teros_12_calibration.csv', skiprows=[0,2])\ndf.head(3)\n\n\n\n\n\n\n\n\n\nsoil\nvwc_obs\nraw_voltage\n\n\n\n\n0\nloam\n0.0047\n1888\n\n\n1\nloam\n0.1021\n2019\n\n\n2\nloam\n0.2538\n2324\n\n\n\n\n\n\n\n\n\n# Fit linear model (degree=1)\npar = P.polyfit(df['raw_voltage'], df['vwc_obs'], deg=1)\nprint(par)\n\n# Polynomial coefficients ordered from low to high.\n\n[-7.94969978e-01 4.32559249e-04]\n\n\n\n# Evaluate fitted linear model at measurement points\ndf['vwc_pred'] = P.polyval(df['raw_voltage'], par)\n\n\n# Determine mean absolute error (MAE)\n\n# Define auxiliary function for MAE\nmae_fn = lambda x,y: np.round(np.mean(np.abs(x-y)),3)\n\n# COmpute MAE for our observations\nmae = mae_fn(df['vwc_obs'], df['vwc_pred'])\nprint('MAE:',mae)\n\nMAE: 0.027\n\n\n\n# Compute coefficient of determination (R^2)\nr2 = r2_score(df['vwc_obs'], df['vwc_pred'])\nprint(\"R-squared:\", np.round(r2, 2))\n\nR-squared: 0.96\n\n\n\n# Create range of voltages (independent variable) to create a line\nn_points = 100\nx_pred = np.linspace(df['raw_voltage'].min(), df['raw_voltage'].max(), n_points)\n\n# Predict values of voluemtric water content (dependent variable) for line\ny_pred = P.polyval(x_pred, par)\n\nFor a linear model we need at least two points and for non-linear models we need more than two. However, creating a few hundred or even a few thaousand values is not that expensive in terms of memory and processing time, so above we adopted a total of 100 points, so that you can adapt this example to non-linear models if necessary.\n\n# Create figure\nfontsize=12\n\nplt.figure(figsize=(5,4))\nplt.title('TEROS 12 - Calibration', size=14)\nplt.scatter(df['raw_voltage'], df['vwc_obs'], facecolor='w', edgecolor='k', s=50, label='Obs')\nplt.plot(x_pred, y_pred, color='black', label='Fitted')\nplt.xlabel('Voltage (mV)', size=fontsize)\nplt.ylabel('VWC Observed (cm³ cm⁻³)', size=fontsize)\nplt.yticks(size=fontsize)\nplt.xticks(size=fontsize)\nplt.text(1870, 0.44, 'B',size=18)\nplt.text(1870, 0.40,f'N = {df.shape[0]}', fontsize=14)\nplt.text(1870, 0.36,f'MAE = {mae} (cm³ cm⁻³)', fontsize=14)\nplt.legend(fontsize=fontsize, loc = 'lower right')\nplt.show()", + "crumbs": [ + "REGRESSION AND CURVE FITTING", + "58  Calibration soil moisture sensor" + ] + }, + { + "objectID": "exercises/neutron_probe_calibration.html#references", + "href": "exercises/neutron_probe_calibration.html#references", + "title": "59  Calibration neutron probe", + "section": "References", + "text": "References\nEvett, S.R., Tolk, J.A. and Howell, T.A., 2003. A depth control stand for improved accuracy with the neutron probe. Vadose Zone Journal, 2(4), pp.642-649.\nPatrignani, A., Godsey, C.B., Ochsner, T.E. and Edwards, J.T., 2012. Soil water dynamics of conventional and no-till wheat in the Southern Great Plains. Soil Science Society of America Journal, 76(5), pp.1768-1775.\nPatrignani, A., Godsey, C.B. and Ochsner, T.E., 2019. No-Till Diversified Cropping Systems for Efficient Allocation of Precipitation in the Southern Great Plains. Agrosystems, Geosciences & Environment, 2(1)." + }, + { + "objectID": "exercises/sorghum_yields.html#read-convert-units-and-explore-dataset", + "href": "exercises/sorghum_yields.html#read-convert-units-and-explore-dataset", + "title": "60  Sorghum historical yields", + "section": "Read, convert units, and explore dataset", + "text": "Read, convert units, and explore dataset\n\n# Read dataset\ndf = pd.read_csv('../datasets/sorghum_yield_kansas.csv')\n\n# Retain only coluns for year and yield value\ndf = df[['Year','Value']]\n\n# Rename columns\ndf.rename(columns={'Year':'year', 'Value':'yield_bu_ac'}, inplace=True)\n\n# Display a few rows\ndf.head()\n\n\n\n\n\n\n\n\nyear\nyield_bu_ac\n\n\n\n\n0\n2023\n52.0\n\n\n1\n2022\n39.0\n\n\n2\n2021\n78.0\n\n\n3\n2020\n85.0\n\n\n4\n2019\n85.0\n\n\n\n\n\n\n\n\n# Convert units (39.368 bu per Mg and 0.405 ac per hectare)\ndf['yield_mg_ha'] = df['yield_bu_ac']/0.405/39.368\ndf.head(3)\n\n\n\n\n\n\n\n\nyear\nyield_bu_ac\nyield_mg_ha\n\n\n\n\n0\n2023\n52.0\n3.261407\n\n\n1\n2022\n39.0\n2.446055\n\n\n2\n2021\n78.0\n4.892110\n\n\n\n\n\n\n\n\n# Visualize dataset\nplt.figure(figsize=(6,4))\nplt.title('Kansas Historical Sorghum Yield')\nplt.plot(df['year'], df['yield_mg_ha'], color='k', marker='o', markersize=5)\nplt.xlabel('Year')\nplt.ylabel('Yield (Mg/ha)')\nplt.show()" + }, + { + "objectID": "exercises/sorghum_yields.html#yield-trend", + "href": "exercises/sorghum_yields.html#yield-trend", + "title": "60  Sorghum historical yields", + "section": "Yield trend", + "text": "Yield trend\nTo synthesize the main yield trends we will subdivide the time series into periods. We know in advance that sorghum hybrids, which have higher yield potential than tradional varieties due to heterosis or hybrid vigor, were introduced in the 1950s. This also coincides with the widespread use of fertilizers. There is also some evidence, from crops like winter wheat, that yields started to show signs of stagnation around 1980s. So, with these tentative years in mind and some visual inspection we will define two breakpoints to start.\n\n# Define year breaks based on visual inspection\nsections = [{'start':1929, 'end':1955, 'ytxt':1.5},\n {'start':1955, 'end':1990, 'ytxt':1.0},\n {'start':1990, 'end':2023, 'ytxt':0.5}]\n\n\n# Fit linear models and create figure\n\nplt.figure(figsize=(6,4))\nplt.plot(df['year'], df['yield_mg_ha'], '-k')\n\nfor k,s in enumerate(sections):\n \n # Select period\n idx = (df['year']>= s['start']) & (df['year']<= s['end'])\n \n # Put X and y variables in shorter names and proper format\n x = df.loc[idx,'year'].values\n y = df.loc[idx,'yield_mg_ha'].values\n \n # Linear regresion using OLS\n slope, intercept, r, p, se = linregress(x, y)\n\n # Plot line\n y_pred = intercept+slope*x\n plt.plot(x, y_pred, color='tomato', linewidth=2)\n\n # Add annotations to chart\n if p<0.01:\n sig = '**'\n elif p<0.05:\n sig='*'\n else:\n sig = ''\n \n # Annotate the chart\n txt = f\"{s['start']}-{s['end']} y = {intercept:.1f} + {slope:.3f}x {sig}\"\n plt.text(1970, s['ytxt'], txt)\n #plt.text(1928, 5.3, 'A', size=20)\n\nplt.xlabel('Year')\nplt.ylabel('Yield (Mg/ha)')\nplt.show()\n\n\n\n\nOne of the main findings of this chart is that the slope of the linear segment from 1990 to 2023 has a non-significat slope, which means that the regression line is not statistically different from zero (this is the null hypothesis), strongly supporting the possibility of yield stagnation." + }, + { + "objectID": "exercises/sorghum_yields.html#optimize-breakpoints", + "href": "exercises/sorghum_yields.html#optimize-breakpoints", + "title": "60  Sorghum historical yields", + "section": "Optimize breakpoints", + "text": "Optimize breakpoints\nA more formal analysis could be done by using a library that optimizes the breakpoints. The piecewise-regression library, which is built-in on top of the statsmodels library is a good option to find breakpoints.\n\n# Get Numpy arrays for x and y variables\nx = df['year'].values\n\n# Compute decadal moving average to smooth extreme yield oscillations\ny_mov_mean = df['yield_mg_ha'].rolling(window=10,\n center=True,\n min_periods=5).mean().values\n\n# Fit piecewise model (here you can try multiple breakpoints\npw_fit = piecewise_regression.Fit(x, y_mov_mean, n_breakpoints=3)\n\n# Create figure to visualize breakpoints\nplt.figure(figsize=(6,4))\n#pw_fit.plot_data(color=\"grey\", s=20)\npw_fit.plot_fit(color=\"tomato\", linewidth=2)\npw_fit.plot_breakpoints()\npw_fit.plot_breakpoint_confidence_intervals()\nplt.plot(df['year'], df['yield_mg_ha'], '-k')\nplt.xlabel('Year')\nplt.ylabel('Yield (Mg/ha)')\nplt.show()\n\n\n\n\nSmoothing the time series using a decadal moving average and then fitting a piecewise linear model using three breakpoints instead of two revealed the sharp yield increase in the late 1950s and early 1960s when sorghum hybrids were introduced into the market, and two additional segments showing the gradual increase in grain yield, but with decreasing slopes, signaling a potential yield stagnation of sorghum yields across Kansas.\n\n# Show stats\nprint(pw_fit.summary())\n\n\n Breakpoint Regression Results \n====================================================================================================\nNo. Observations 95\nNo. Model Parameters 8\nDegrees of Freedom 87\nRes. Sum of Squares 1.41455\nTotal Sum of Squares 201.084\nR Squared 0.992965\nAdjusted R Squared 0.992311\nConverged: True\n====================================================================================================\n====================================================================================================\n Estimate Std Err t P>|t| [0.025 0.975]\n----------------------------------------------------------------------------------------------------\nconst -52.9074 6.47 -8.1729 2.2e-12 -65.774 -40.041\nalpha1 0.0277171 0.00333 8.3128 1.14e-12 0.02109 0.034344\nbeta1 0.133628 0.0168 7.956 - 0.10024 0.16701\nbeta2 -0.108239 0.0168 -6.4443 - -0.14162 -0.074855\nbeta3 -0.0369933 0.00401 -9.2236 - -0.044965 -0.029022\nbreakpoint1 1954.44 0.746 - - 1953.0 1955.9\nbreakpoint2 1963.37 0.902 - - 1961.6 1965.2\nbreakpoint3 1989.95 1.81 - - 1986.4 1993.5\n----------------------------------------------------------------------------------------------------\nThese alphas(gradients of segments) are estimatedfrom betas(change in gradient)\n----------------------------------------------------------------------------------------------------\nalpha2 0.161346 0.0165 9.8013 1.03e-15 0.12863 0.19406\nalpha3 0.0531067 0.00333 15.927 1.6e-27 0.046479 0.059734\nalpha4 0.0161134 0.00223 7.229 1.77e-10 0.011683 0.020544\n====================================================================================================\n\n\n\n\n Breakpoint Regression Results \n====================================================================================================\nNo. Observations 95\nNo. Model Parameters 8\nDegrees of Freedom 87\nRes. Sum of Squares 1.41455\nTotal Sum of Squares 201.084\nR Squared 0.992965\nAdjusted R Squared 0.992311\nConverged: True\n====================================================================================================\n====================================================================================================\n Estimate Std Err t P>|t| [0.025 0.975]\n----------------------------------------------------------------------------------------------------\nconst -52.9074 6.47 -8.1729 2.2e-12 -65.774 -40.041\nalpha1 0.0277171 0.00333 8.3128 1.14e-12 0.02109 0.034344\nbeta1 0.133628 0.0168 7.956 - 0.10024 0.16701\nbeta2 -0.108239 0.0168 -6.4443 - -0.14162 -0.074855\nbeta3 -0.0369933 0.00401 -9.2236 - -0.044965 -0.029022\nbreakpoint1 1954.44 0.746 - - 1953.0 1955.9\nbreakpoint2 1963.37 0.902 - - 1961.6 1965.2\nbreakpoint3 1989.95 1.81 - - 1986.4 1993.5\n----------------------------------------------------------------------------------------------------\nThese alphas(gradients of segments) are estimatedfrom betas(change in gradient)\n----------------------------------------------------------------------------------------------------\nalpha2 0.161346 0.0165 9.8013 1.03e-15 0.12863 0.19406\nalpha3 0.0531067 0.00333 15.927 1.6e-27 0.046479 0.059734\nalpha4 0.0161134 0.00223 7.229 1.77e-10 0.011683 0.020544\n====================================================================================================" + }, + { + "objectID": "exercises/sorghum_yields.html#yield-variance", + "href": "exercises/sorghum_yields.html#yield-variance", + "title": "60  Sorghum historical yields", + "section": "Yield variance", + "text": "Yield variance\nWhile the exact breaks for the linear regression can be debatable, one aspect that seems clear in this dataset is the increasing variance of grain yields. One possible reason could be the increased temporal variability of environmental variables directly related to grain yield, like growing season precipitation. This is clearly seen in later decades, where state-level yiedlds have declined dramatically. Another possible reason for the increasing yield variability could be the shifting of sorghum planted area, from wetter to drier portions of the state. But the specific reason, whether due to climatological or geographical factors, remains to be explored.\n\nDe-trending yield\nOne way to visualize yield variation over time is to plot the detrended time series.\n\n# Subtract piecewise linear model from yield time series\nyield_detrended = df['yield_mg_ha'] - pw_fit.yy\n\n# Create figure\nplt.figure(figsize=(6,4))\nplt.plot(df['year'], yield_detrended)\nplt.xlabel('Year')\nplt.ylabel('Detrended yield (Mg/ha)')\nplt.show()\n\n\n\n\n\n\nMoving variance\nAnother option to visualize the variance of a time series is to compute the moving variance.\n\n# Moving variance\nmov_var = df['yield_mg_ha'].rolling(window=10,center=True,min_periods=1).var()\n\n# Create figure\nplt.figure(figsize=(6,4))\nplt.plot(df['year'], mov_var, color='r')\nplt.xlabel('Year')\nplt.ylabel('Yield variance')\nplt.show()" + }, + { + "objectID": "exercises/multiple_linear_regression.html", + "href": "exercises/multiple_linear_regression.html", + "title": "61  Multiple linear regression", + "section": "", + "text": "Multiple linear regression analysis is a statistical technique used to model the relationship between two or more independent variables (x) and a single dependent variable (y). By fitting a linear equation to observed data, this method allows for the prediction of the dependent variable based on the values of the independent variables. It extends simple linear regression, which involves only one independent variable, to include multiple factors, providing a more comprehensive understanding of complex relationships within the data. The general formula is: y = \\beta_0 \\ + \\beta_1 \\ x_1 + ... + \\beta_n \\ x_n\nAn agronomic example that involves the use of multiple linear regression are allometric measurements, such as estimating corn biomass based on plant height and stem diameter.\n\n# Import modules\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport statsmodels.api as sm\n\n\n# Read dataset\ndf = pd.read_csv(\"../datasets/corn_allometric_biomass.csv\")\ndf.head(3)\n\n\n\n\n\n\n\n\nheight_cm\nstem_diam_mm\ndry_biomass_g\n\n\n\n\n0\n71.0\n5.7\n0.66\n\n\n1\n39.0\n4.4\n0.19\n\n\n2\n55.5\n4.3\n0.30\n\n\n\n\n\n\n\n\n# Re-define variables for better plot semantics and shorter variable names\nx_data = df['stem_diam_mm'].values\ny_data = df['height_cm'].values\nz_data = df['dry_biomass_g'].values\n\n\n# Plot raw data using 3D plots.\n# Great tutorial: https://jakevdp.github.io/PythonDataScienceHandbook/04.12-three-dimensional-plotting.html\n\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n#ax = fig.add_subplot(projection='3d')\n\n\n# Define axess\nax.scatter3D(x_data, y_data, z_data, c='r');\nax.set_xlabel('Stem diameter (mm)') \nax.set_ylabel('Plant height (cm)')\nax.set_zlabel('Dry biomass (g)')\n\nax.view_init(elev=20, azim=100)\nplt.show()\n\n# elev=None, azim=None\n# elev = elevation angle in the z plane.\n# azim = stores the azimuth angle in the x,y plane.\n\n\n\n\n\n# Full model\n\n# Create array of intercept values\n# We can also use X = sm.add_constant(X)\nintercept = np.ones(df.shape[0])\n \n# Create matrix with inputs (rows represent obseravtions and columns the variables)\nX = np.column_stack((intercept, \n x_data,\n y_data,\n x_data * y_data)) # interaction term\n\n# Print a few rows\nprint(X[0:3,:])\n\n[[ 1. 5.7 71. 404.7 ]\n [ 1. 4.4 39. 171.6 ]\n [ 1. 4.3 55.5 238.65]]\n\n\n\n# Run Ordinary Least Squares to fit the model\nmodel = sm.OLS(z_data, X)\nresults = model.fit()\nprint(results.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.849\nModel: OLS Adj. R-squared: 0.836\nMethod: Least Squares F-statistic: 63.71\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 4.87e-14\nTime: 14:12:19 Log-Likelihood: -129.26\nNo. Observations: 38 AIC: 266.5\nDf Residuals: 34 BIC: 273.1\nDf Model: 3 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 18.8097 6.022 3.124 0.004 6.572 31.048\nx1 -4.5537 1.222 -3.727 0.001 -7.037 -2.070\nx2 -0.1830 0.119 -1.541 0.133 -0.424 0.058\nx3 0.0433 0.007 6.340 0.000 0.029 0.057\n==============================================================================\nOmnibus: 9.532 Durbin-Watson: 2.076\nProb(Omnibus): 0.009 Jarque-Bera (JB): 9.232\nSkew: 0.861 Prob(JB): 0.00989\nKurtosis: 4.692 Cond. No. 1.01e+04\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 1.01e+04. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nheight (x1) does not seem to be statistically significant. This term has a p-value > 0.05 and the range of the 95% confidence interval for its corresponding \\beta coefficient includes zero.\nThe goal is to prune the full model by removing non-significant terms. After removing these terms, we need to fit the model again to update the new coefficients.\n\n# Define prunned model\nX_prunned = np.column_stack((intercept, \n x_data, \n x_data * y_data))\n\n# Re-fit the model\nmodel_prunned = sm.OLS(z_data, X_prunned)\nresults_prunned = model_prunned.fit()\nprint(results_prunned.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.838\nModel: OLS Adj. R-squared: 0.829\nMethod: Least Squares F-statistic: 90.81\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 1.40e-14\nTime: 14:13:05 Log-Likelihood: -130.54\nNo. Observations: 38 AIC: 267.1\nDf Residuals: 35 BIC: 272.0\nDf Model: 2 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 14.0338 5.263 2.666 0.012 3.349 24.719\nx1 -5.0922 1.194 -4.266 0.000 -7.515 -2.669\nx2 0.0367 0.005 6.761 0.000 0.026 0.048\n==============================================================================\nOmnibus: 12.121 Durbin-Watson: 2.194\nProb(Omnibus): 0.002 Jarque-Bera (JB): 13.505\nSkew: 0.997 Prob(JB): 0.00117\nKurtosis: 5.135 Cond. No. 8.80e+03\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 8.8e+03. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nThe prunned model has: - r-squared remains similar - one less parameter - higher F-Statistic 91 vs 63 - AIC remains similar (the lower the better)\n\n# Access parameter/coefficient values\nprint(results_prunned.params)\n\n[14.03378102 -5.09215874 0.03671944]\n\n\n\n# Create surface grid\n\n# Xgrid is grid of stem diameter\nx_vec = np.linspace(x_data.min(), x_data.max(), 21)\n\n# Ygrid is grid of plant height\ny_vec = np.linspace(y_data.min(), y_data.max(), 21)\n\n# We generate a 2D grid\nX_grid, Y_grid = np.meshgrid(x_vec, y_vec)\n\n# Create intercept grid\nintercept = np.ones(X_grid.shape)\n\n# Get parameter values\npars = results_prunned.params\n\n# Z is the elevation of this 2D grid\nZ_grid = intercept*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]\n\nAlternatively you can use the .predict() method of the fitted object. This option would required flattening the arrays to make predictions:\nX_pred = np.column_stack((intercept.flatten(), X_grid.flatten(), X_grid.flatten() * Y_grid.flatten()) )\nZ_grid = model_prunned.predict(params=results_prunned.params, exog=X_pred)\nZ_grid = np.reshape(Z_grid, X_grid.shape) # Reset shape to match \n\n# Plot points with predicted model (which is a surface)\n\n# Create figure and axes\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n\nax.scatter3D(x_data, y_data, z_data, c='r', s=80);\nsurf = ax.plot_wireframe(X_grid, Y_grid, Z_grid, color='black')\n#surf = ax.plot_surface(Xgrid, Ygrid, Zgrid, cmap='green', rstride=1, cstride=1)\n\nax.set_xlabel('Stem diameter (mm)')\nax.set_ylabel('Plant height x stem diameter')\nax.set_zlabel('Dry biomass (g)')\nax.view_init(20, 130)\nfig.tight_layout()\nax.set_box_aspect(aspect=None, zoom=0.8) # Zoom out to see zlabel\nax.set_zlim([0,100])\nplt.show()\n\n\n\n\n\n\n# We can now create a lambda function to help use convert other observations\ncorn_biomass_fn = lambda stem,height: pars[0] + stem*pars[1] + stem*height*pars[2]\n\n\n# Test the lambda function using stem = 11 mm and height = 120 cm\nbiomass = corn_biomass_fn(11, 120)\nprint(round(biomass,2), 'g')\n\n6.49 g" + }, + { + "objectID": "exercises/proctor_test.html#references", + "href": "exercises/proctor_test.html#references", + "title": "62  Proctor test", + "section": "References", + "text": "References\nDavidson, J.M., Gray, F. and Pinson, D.I., 1967. Changes in Organic Matter and Bulk Density with Depth Under Two Cropping Systems 1. Agronomy Journal, 59(4), pp.375-378.\nKok, H., Taylor, R.K., Lamond, R.E., and Kessen, S.1996. Soil Compaction: Problems and Solutions. Department of Agronomy. Publication no. AF-115 by the Kansas State University Cooperative Extension Service. Manhattan, KS. You can find the article at this link: https://bookstore.ksre.ksu.edu/pubs/AF115.pdf\nProctor, R., 1933. Fundamental principles of soil compaction. Engineering news-record, 111(13)." + }, + { + "objectID": "exercises/optimal_nitrogen_rate.html", + "href": "exercises/optimal_nitrogen_rate.html", + "title": "63  Optimal nitrogen rate", + "section": "", + "text": "Dataset description: Fertilized corn crop in Nebraska.\nCorn Trial conducted jointly with Dennis Francis and Jim Schepers, USDA-ARS, University of Nebraska, Lincoln, NE. Corn trial located near Shelton, NE. Harvested in October, 2003\nSource: http://www.nue.okstate.edu/Nitrogen_Conference2003/Corn_Research.htm\n\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom numpy.polynomial import polynomial as P\n\n\n# Define auxiliary function for computing the root mean squared error (RMSE)\nrmse_fn = lambda obs,pred: np.sqrt(np.mean((obs - pred)**2))\n\n\n# Fit polynomial: Example using yield responses to nitrogen rates\nyield_obs = [118, 165, 170, 193, 180, 181, 141, 177, 165, 197, 175] # Corn yield\nnitrogen_obs = [0, 89, 161, 165, 80, 160, 37, 105, 69, 123, 141]\n\n\n# Visualize field observations\n\nplt.figure(figsize=(5,4))\nplt.scatter(nitrogen_obs, yield_obs, facecolor='w', edgecolor='k')\nplt.xlabel('Nitrogen rate (kg/ha)')\nplt.ylabel('Yield rate (kg/ha)')\nplt.show()\n\n\n\n\n\n# Fit polynomial model\npar = P.polyfit(nitrogen_obs, yield_obs, 2)\nprint(par)\n\n# Use P.polyval? to access function help\n\n[ 1.15569115e+02 9.61329119e-01 -3.41221057e-03]\n\n\n\n# Compute fitting error\n\n# Compute \nyield_pred = P.polyval(nitrogen_obs, par)\n\nrmse = rmse_fn(yield_obs, yield_pred)\nprint(round(rmse,1),'kg/ha')\n\n8.4 kg/ha\n\n\n\n# Compute fitted curve\nN_min = np.min(nitrogen_obs)\nN_max = np.max(nitrogen_obs)\n\nnitrogen_curve = np.linspace(N_min, N_max)\nyield_curve = P.polyval(nitrogen_curve, par)\n\n\n# Find index at which yield in the nitrogen curve is highest\nidx_yield_max = np.argmax(yield_curve)\n\n# Use the index to retrieve the optinal nitrogen rate at which yield is maximum\nN_opt = nitrogen_curve[idx_yield_max]\n\n# Find min and max yield\nY_max = np.max(yield_curve)\nY_min = np.min(yield_curve)\n\nprint(f'The optimal N rate is {N_opt:.1f} kg/ha')\n\nThe optimal N rate is 141.4 kg/ha\n\n\nWe can also find the optimal Nitrogen rate by approximating the first derivative of the function. Note that np.diff() will return an array that is one-element shorter.\nidx = np.argmin(np.abs(np.diff(yield_curve, 1)))\nnitrogen_curve[idx+1]\n\n# Visualize field observations\nplt.figure(figsize=(5,4))\nplt.scatter(nitrogen_obs, yield_obs, facecolor='w', edgecolor='k', label='Observations')\nplt.plot(nitrogen_curve, yield_curve, \n color='tomato',linestyle='dashed', linewidth=2, label='Fit')\n\nplt.axvline(N_opt, color='k')\nplt.xlabel('Nitrogen rate (kg/ha)')\nplt.ylabel('Yield rate (kg/ha)')\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/maximum_return_to_nitrogen.html", + "href": "exercises/maximum_return_to_nitrogen.html", + "title": "64  Maximum return to nitrogen application", + "section": "", + "text": "An important decision that farmers have to make during the growing season is decide the amount of nitrogen fertilizer that needs to be applied to the crop. Multiple factors contribute to this decision including the potential yield of the crop, the price of the harvestable part of the crop, the cost of nitrogen fertilizer, the current amount of nitrogen in the soil, and the nitrogen requirements of the crop.\nThe maximum return to nitrogen rate is one way to balance the estimated gross revenue and the cost of the input fertilizer. For this method to work, a yield response function to nitrogen is essential because it determines the amount of yield increase per unit input added to the crop, until a point where a new unit of nitrogen fertilizer does not produce gross revenue to pay for itself.\nWe will use the yield response function to nitrogen define in the previous exercise as an example.\n\n# Import modules\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\n# Define inputs\ngrain_price = 0.17 # US$ per kg of grain\nfertilizer_cost = 0.02 # US$ per kg of nitrogen\ngrain_fertilizer_ratio = grain_price/fertilizer_cost\nprint(grain_fertilizer_ratio)\n\n8.5\n\n\n\n# Define yield response function\ndef responsefn(nitrogen_input):\n beta_0 = 115.6\n beta_1 = 0.9613\n beta_2 = -0.003412\n x_critical = -beta_1/(2*beta_2)\n \n Y = []\n for N in nitrogen_input:\n if (N<x_critical):\n Y.append( beta_0 + beta_1*N + beta_2*N**2 )\n else:\n Y.append( beta_0 - beta_1**2/(4*beta_2) )\n\n return np.array(Y)\n\n\n# Find maximum return to nitrogen\nnitrogen_range = np.arange(200)\nyield_range = responsefn(nitrogen_range)\ngross_revenue = yield_range * grain_price\nvariable_costs = nitrogen_range * fertilizer_cost\nnet_revenue = gross_revenue - variable_costs\nidx = np.argmax(net_revenue)\n\nprint(nitrogen_range[idx],'kg per hectare')\n\n124 kg per hectare\n\n\n\n# Compute maximum nitrogen rate\nplt.figure(figsize=(6,4))\nplt.plot(nitrogen_range, gross_revenue, color='royalblue', linewidth=2)\nplt.plot(nitrogen_range, variable_costs, color='tomato', linewidth=2)\nplt.plot(nitrogen_range, net_revenue, color='green', linewidth=2)\nplt.scatter(nitrogen_range[idx], net_revenue[idx], facecolor='green')\nplt.xlabel('Nitrogen Rate (kg/ha)')\nplt.ylabel('Return to Nitrogen ($/ha)')\nplt.show()" + }, + { + "objectID": "exercises/solar_radiation.html#inspect-timeseries", + "href": "exercises/solar_radiation.html#inspect-timeseries", + "title": "65  Solar Radiation", + "section": "Inspect timeseries", + "text": "Inspect timeseries\n\n# Observe trends in solar radiation data\nplt.figure(figsize=(12,4))\nplt.plot(df[\"LST_DATE\"], df[\"SOLARAD_DAILY\"], linewidth=0.5)\nplt.ylabel(\"Solar radiation (MJ/m$^2$/day)\" )\nplt.show()" + }, + { + "objectID": "exercises/solar_radiation.html#clear-sky-solar-irradiance-empirical-method", + "href": "exercises/solar_radiation.html#clear-sky-solar-irradiance-empirical-method", + "title": "65  Solar Radiation", + "section": "Clear sky solar irradiance: empirical method", + "text": "Clear sky solar irradiance: empirical method\nTo approximate the clear sky solar irradiance we will select the highest records from our observations. To do this we will use a moving/rolling percentile filter. Alternatively, we can use the .max() function instead of the .quantile(0.99) function.\n\n# clear sky solar radiation from observations\ndf[\"Rso_obs\"] = df[\"SOLARAD_DAILY\"].rolling(window=15, center=True).quantile(0.99)\n\n# Observe trends in solar radiation data\nplt.figure(figsize=(12,4))\nplt.plot(df[\"LST_DATE\"], df[\"SOLARAD_DAILY\"], '-k', alpha=0.25)\nplt.plot(df[\"LST_DATE\"], df[\"Rso_obs\"], '-r')\nplt.ylabel(\"Solar radiation (MJ/m$^2$/day)\" )\nplt.show()" + }, + { + "objectID": "exercises/solar_radiation.html#clear-sky-solar-irradiance-latitude-and-elevation-method", + "href": "exercises/solar_radiation.html#clear-sky-solar-irradiance-latitude-and-elevation-method", + "title": "65  Solar Radiation", + "section": "Clear sky solar irradiance: latitude and elevation method", + "text": "Clear sky solar irradiance: latitude and elevation method\nAnother alternative is to compute the clear sky solar irradiance based on the latitude and elevation of the location of interest. The first step consists of computing the extraterrestrial radiation for daily periods as defined in Eq. 21 of the FAO-56 manual, and in a subsequent step we compute the clear sky solar radiation.\nRa = 24(60)/\\pi \\hspace{2mm}Gsc \\hspace{2mm} dr(\\omega\\sin(\\phi)\\sin(\\delta)+\\cos(\\phi)\\cos(\\delta)\\sin(\\omega))\nRa = extraterrestrial radiation (MJ / m2 /day)\nGsc = 0.0820 solar constant (MJ/m2/min)\ndr = 1 + 0.033\\cos(\\frac{2\\pi J}{365}) is the inverse relative distance Earth-Sun\nJ = day of the year\n\\phi = \\pi/180 Lat latitude in radians\n\\delta = 0.409\\sin((2\\pi J/365)-1.39)\\hspace{5mm} is the solar decimation (rad)\n\\omega = \\pi/2-(\\arccos(-\\tan(\\phi)\\tan(\\delta)) \\hspace{5mm} is the sunset hour angle (radians)\n\nlatitude = 39.1949 # Latitude in decimal degrees (North)\nelevation = 300 # elevation in meters above sea level\nJ = df[\"DOY\"] # Use J to match manual notation\n\n# Step 1: Extraterrestrial solar radiation\nphi = np.pi/180 * latitude # Eq. 22, FAO-56 \ndr = 1 + 0.033 * np.cos(2*np.pi*J/365) # Eq. 23, FAO-56 \nd = 0.409*np.sin((2*np.pi * J/365) - 1.39)\nomega = (np.arccos(-np.tan(phi)*np.tan(d)))\nGsc = 0.0820\nRa = 24*(60)/np.pi * Gsc * dr * (omega*np.sin(phi)*np.sin(d) + np.cos(phi)*np.cos(d)*np.sin(omega))\n\n# Step 2: Clear Sky Radiation: Rso (MJ/m2/day) \ndf[\"Rso_lat\"] = (0.75 + (2*10**-5)*elevation)*Ra # Eq. 37, FAO-56\n\n\n# Plot clear sky using latitude and elevation\nplt.figure(figsize=(12,4))\nplt.plot(df[\"LST_DATE\"], df[\"SOLARAD_DAILY\"], '-k', alpha=0.25)\nplt.plot(df[\"LST_DATE\"], df[\"Rso_lat\"], '-r', linewidth=2)\nplt.ylabel(\"Solar radiation (MJ/m$^2$/day)\" )\nplt.show()" + }, + { + "objectID": "exercises/solar_radiation.html#actual-solar-irradiance-from-air-temperature", + "href": "exercises/solar_radiation.html#actual-solar-irradiance-from-air-temperature", + "title": "65  Solar Radiation", + "section": "Actual solar irradiance from air temperature", + "text": "Actual solar irradiance from air temperature\n\n# Clear sky from air temperature observations\ndf[\"Rso_temp\"] = np.minimum(0.16*Ra*(df[\"T_DAILY_MAX\"]-df[\"T_DAILY_MIN\"])**0.5, df['Rso_lat']) # Eq. 50, FAO-56\n\nplt.figure(figsize=(12,4))\nplt.plot(df[\"LST_DATE\"], df[\"SOLARAD_DAILY\"], '-k', alpha=0.25)\nplt.plot(df[\"LST_DATE\"], df[\"Rso_temp\"], '-r', alpha=0.5)\nplt.ylabel(\"Solar radiation (MJ/m$^2$/day)\" )\nplt.show()" + }, + { + "objectID": "exercises/solar_radiation.html#clear-sky-solar-radiation-for-each-doy", + "href": "exercises/solar_radiation.html#clear-sky-solar-radiation-for-each-doy", + "title": "65  Solar Radiation", + "section": "Clear sky solar radiation for each DOY", + "text": "Clear sky solar radiation for each DOY\n\n# Summarize previous variables for each DOY\nRso_doy = df.groupby(\"DOY\")[[\"SOLARAD_DAILY\",\"Rso_temp\",\"Rso_obs\",\"Rso_lat\"]].mean()\nRso_doy.head()\n\n\n\n\n\n\n\n\nSOLARAD_DAILY\nRso_temp\nRso_obs\nRso_lat\n\n\nDOY\n\n\n\n\n\n\n\n\n1\n7.840714\n7.915730\n10.522923\n10.834404\n\n\n2\n7.135000\n8.016919\n10.558046\n10.876334\n\n\n3\n6.400769\n8.236705\n10.634077\n10.921631\n\n\n4\n6.673077\n6.975503\n10.738108\n10.970287\n\n\n5\n7.897692\n7.595476\n10.751031\n11.022290\n\n\n\n\n\n\n\n\n# Create figure comparing the three methods with the observed solar radaition data\nplt.figure(figsize=(8,4))\nplt.plot(Rso_doy.index, Rso_doy[\"SOLARAD_DAILY\"], '-k',label=\"Mean observed solar radiation\")\nplt.plot(Rso_doy.index, Rso_doy[\"Rso_temp\"], '-g', label=\"Mean estimated solar radiation\")\n\nplt.plot(Rso_doy.index, Rso_doy[\"Rso_obs\"], '--r', label=\"Clear sky observed solar radiation\")\nplt.plot(Rso_doy.index, Rso_doy[\"Rso_lat\"], ':b', label=\"Clear sky estimated solar radiation\")\n\nplt.xlabel(\"Day of the year\")\nplt.ylabel(\"Solar radiation (MJ/m$^2$/day)\")\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/soil_water_retention_curve.html#define-soil-water-retetnion-models", + "href": "exercises/soil_water_retention_curve.html#define-soil-water-retetnion-models", + "title": "66  Soil water retention curves", + "section": "Define soil water retetnion models", + "text": "Define soil water retetnion models\n\nBrooks and Corey model (1964)\n \\frac{\\theta - \\theta_r}{\\theta_s - \\theta_r} = \\Bigg( \\frac{\\psi_e}{\\psi} \\Bigg)^\\lambda \nwhere:\n\\theta is the volumetric water content (cm^3/cm^3) \\theta_r is the residual water content (cm^3/cm^3) \\theta_s is the saturation water content (cm^3/cm^3) \\psi is the matric potential (kPa) \\psi_e is the air-entry suction (kPa) \\lambda is a parameter related to the pore-size distribution\n\ndef brooks_corey_model(x,alpha,lmd,psi_e,theta_r,theta_s):\n \"\"\"\n Function that computes volumetric water content from soil matric potential\n using the Brooks and Corey (1964) model.\n \"\"\"\n theta = np.minimum(theta_r + (theta_s-theta_r)*(psi_e/x)**(lmd), theta_s)\n return theta\n\n\n\nvan Genuchten model (1980)\n \\frac{\\theta - \\theta_r}{\\theta_s - \\theta_r} = [1 + (-\\alpha \\psi)^n]^{-m} \nwhere:\n\\alpha is a fitting parameter inversely related to \\psi_e (kPa^{-1}) n is a fitting parameter m is a fitting parameter that is often assumed to be M=1-1/n, but in this example it was left as a free parameter (see article by Groenevelt and Grant (2004) for mo details.\n\ndef van_genuchten_model(x,alpha,n,m,theta_r,theta_s):\n \"\"\"\n Function that computes volumetric water content from soil matric potential\n using the van Genuchten (1980) model.\n \"\"\"\n theta = theta_r + (theta_s-theta_r)*(1+(alpha*x)**n)**-(1-1/n)\n return theta\n\n\n\nKosugi model (1994)\n \\theta = \\theta_r + \\frac{1}{2} (\\theta_s - \\theta_r) \\ erfc \\Bigg( \\frac{ln(\\psi/ \\psi_m)}{\\sigma \\sqrt(2)} \\Bigg)\nwhere:\nerfc is the complementary error function \\sigma is the standard deviation of ln(\\psi_{med}) \\psi_m is the median matric potential\n\ndef kosugi_model(x,hm,sigma,theta_r,theta_s):\n \"\"\"\n Function that computes volumetric water content from soil matric potential\n using the Kosugi (1994) model. Function was implemented accoridng \n to Eq. 3 in Pollacco et al., 2017.\n \"\"\"\n theta = theta_r + 1/2*(theta_s-theta_r)*erfc(np.log(x/hm)/(sigma*np.sqrt(2))) \n return theta\n\n\n\nGroenvelt-Grant model (2004)\n \\theta = k_1 \\Bigg[ exp\\Bigg( \\frac{-k_0}{\\big(10^{5.89}\\big)^n} \\Bigg) - exp\\Bigg( \\frac{-k_0}{\\psi ^n} \\Bigg) \\Bigg]\nwhere:\nk_0, k_1, and n are fitting parameters. The value 10^{5.89} is the matric potential in kPa at oven-dry conditions (105 degrees Celsius)\n\ndef groenevelt_grant_model(x,k0,k1,n,theta_s):\n \"\"\"\n Function that computes volumetric water content from soil matric potential \n using the Groenevelt-Grant (2004) model.\n \"\"\"\n theta = k1 * ( np.exp(-k0/ (10**5.89)**n) - np.exp(-k0/(x**n)) ) # Eq. 5 in Groenevelt and Grant, 2004\n return theta" + }, + { + "objectID": "exercises/soil_water_retention_curve.html#define-error-models", + "href": "exercises/soil_water_retention_curve.html#define-error-models", + "title": "66  Soil water retention curves", + "section": "Define error models", + "text": "Define error models\n\n# Error models\nmae_fn = lambda x,y: np.round(np.mean(np.abs(x-y)),3)\nrmse_fn = lambda x,y: np.round(np.sqrt(np.mean((x-y)**2)),3)\n\n\n1-1/1.5\n\n0.33333333333333337" + }, + { + "objectID": "exercises/soil_water_retention_curve.html#fit-soil-water-retention-models-to-dataset", + "href": "exercises/soil_water_retention_curve.html#fit-soil-water-retention-models-to-dataset", + "title": "66  Soil water retention curves", + "section": "Fit soil water retention models to dataset", + "text": "Fit soil water retention models to dataset\n\n# Define variables\nx_obs = df[\"matric\"]\ny_obs = df[\"theta\"]\nx_curve = np.logspace(-1.5,5,1000)\n\n# Empty list to collect output of each model\noutput = []\n\n# van Genuchten model\np0 = [0.02,1.5,1,0.1,0.5]\nbounds = ([0.001,1,0,0,0.3], [1,10,25,0.3,0.6])\npar_opt, par_cov = curve_fit(van_genuchten_model, x_obs, y_obs, p0=p0, bounds=bounds)\ny_curve = van_genuchten_model(x_curve, *par_opt)\noutput.append({'name':'van Genuchten',\n 'y_curve':y_curve,\n 'mae':mae_fn(van_genuchten_model(x_obs, *par_opt), y_obs),\n 'rmse':rmse_fn(van_genuchten_model(x_obs, *par_opt), y_obs),\n 'par_values':par_opt,\n 'par_names':van_genuchten_model.__code__.co_varnames,\n 'color':'black'})\n \n# Brooks and Corey\np0=[0.02, 1, 10, 0.1, 0.5]\nbounds=([0.001, 0.1, 1, 0, 0.3], [1, 10, 100, 0.3, 0.6])\npar_opt, par_cov = curve_fit(brooks_corey_model, x_obs, y_obs, p0=p0, bounds=bounds)\ny_curve = brooks_corey_model(x_curve, *par_opt)\noutput.append({'name':'Brooks and Corey',\n 'y_curve':y_curve,\n 'mae':mae_fn(brooks_corey_model(x_obs, *par_opt), y_obs),\n 'rmse':rmse_fn(brooks_corey_model(x_obs, *par_opt), y_obs),\n 'par_values':par_opt,\n 'par_names':brooks_corey_model.__code__.co_varnames,\n 'color':'tomato'})\n\n\n# Kosugi\np0=[50, 1, 0.1, 0.5]\nbounds=([1, 1, 0, 0.3], [500, 10, 0.3, 0.6])\npar_opt, par_cov = curve_fit(kosugi_model, x_obs, y_obs, p0=p0, bounds=bounds)\ny_curve = kosugi_model(x_curve, *par_opt)\noutput.append({'name':'Kosugi',\n 'y_curve':y_curve,\n 'mae':mae_fn(kosugi_model(x_obs, *par_opt), y_obs),\n 'rmse':rmse_fn(kosugi_model(x_obs, *par_opt), y_obs),\n 'par_values':par_opt,\n 'par_names':kosugi_model.__code__.co_varnames,\n 'color':'darkgreen'})\n\n\n# Groenevelt-Grant\np0=[5, 1, 2, 0.5]\nbounds=([1, 0.1, 0.1, 0.3], [2000, 10, 5, 0.6])\npar_opt, par_cov = curve_fit(groenevelt_grant_model, x_obs, y_obs, p0=p0, bounds=bounds)\ny_curve = groenevelt_grant_model(x_curve, *par_opt)\noutput.append({'name':'Groenevelt-Grant',\n 'y_curve':y_curve,\n 'mae':mae_fn(groenevelt_grant_model(x_obs, *par_opt), y_obs),\n 'rmse':rmse_fn(groenevelt_grant_model(x_obs, *par_opt), y_obs),\n 'par_values':par_opt,\n 'par_names':groenevelt_grant_model.__code__.co_varnames,\n 'color':'lightblue'})\n\n\n# Create figure\n# Plot results\nplt.figure(figsize=(6,4))\nfor model in output:\n plt.plot(x_curve, model['y_curve'], color=model['color'],\n linewidth=1.5, label=model['name'])\nplt.scatter(x_obs, y_obs, marker='o', facecolor='w', \n alpha=1, edgecolor='k', zorder=10, label='Observations')\nplt.xscale('log')\nplt.xlabel('$|\\psi_m|$ (kPa)', size=12)\nplt.ylabel('Volumetric water content (cm$^3$/cm$^3$)', size=12)\nplt.xticks(fontsize=12)\nplt.yticks(fontsize=12)\nplt.xlim([0.01, 100_000])\nplt.legend()\nplt.show()\n\n\n\n\n\n# Print parameters\nfor k,model in enumerate(output):\n print(model['name'])\n for par_name, par_value in list(zip(model['par_names'][1:], model['par_values'])):\n print(par_name, '=', par_value)\n print('')\n\nvan Genuchten\nalpha = 0.03982164458016106\nn = 1.4290698142534732\nm = 15.450501882131682\ntheta_r = 3.797456144597235e-21\ntheta_s = 0.4376343934467054\n\nBrooks and Corey\nalpha = 0.02\nlmd = 0.25592618085795293\npsi_e = 10.068459416938797\ntheta_r = 2.402575753992586e-21\ntheta_s = 0.430430826679669\n\nKosugi\nhm = 122.18916515441454\nsigma = 1.962113379238266\ntheta_r = 4.989241799635874e-22\ntheta_s = 0.44660152453875435\n\nGroenevelt-Grant\nk0 = 8.104570970997068\nk1 = 0.44363145088075745\nn = 0.502979559118283\ntheta_s = 0.5" + }, + { + "objectID": "exercises/soil_water_retention_curve.html#references", + "href": "exercises/soil_water_retention_curve.html#references", + "title": "66  Soil water retention curves", + "section": "References", + "text": "References\nBrooks, R. H. (1965). Hydraulic properties of porous media. Colorado State University.\nGroenevelt, P.H. and Grant, C.D., 2004. A new model for the soil‐water retention curve that solves the problem of residual water contents. European Journal of Soil Science, 55(3), pp.479-485.\nKosugi, K. I. (1994). Three‐parameter lognormal distribution model for soil water retention. Water Resources Research, 30(4), 891-901.\nPollacco, J. A. P., Webb, T., McNeill, S., Hu, W., Carrick, S., Hewitt, A., & Lilburne, L. (2017). Saturated hydraulic conductivity model computed from bimodal water retention curves for a range of New Zealand soils. Hydrology and Earth System Sciences, 21(6), 2725-2737.\nvan Genuchten, M.T., 1980. A closed form equation for predicting hydraulic conductivity of unsaturated soils: Journal of the Soil Science Society of America." + }, + { + "objectID": "exercises/frontier_function.html#curve-interpretation", + "href": "exercises/frontier_function.html#curve-interpretation", + "title": "67  Frontier production functions", + "section": "Curve interpretation", + "text": "Curve interpretation\n\nThe part of the frontier intercepting the y axis is negative. We restricted the plot to plausible yield values, which are certainly greater than zero.\nThe part of the frontier that intercepts the x axis represents the amount of growing season water supply that genertes zero yield. This can be viewed as an inefficiency of the system and represents the minimum water losses (due to runoff, evaporation, drainage, canopy interception, etc.).\nThe highest point of the curve gives us an idea of the growing season rainfall required to achieve the highest yeilds. Note that during many years yields in the central range can be much lower than the highest yield. To a great extent this associated to other factors, such as the distribution of the rainfall during the growing season or the occurrence of other factors like heat stress, hail damage, or yield losses due to diseases and pests.\nThe last, and decaying, portion of the curve could be due to two main reasons: 1) There is insuficient yield data for years with high growing season rainfall. The chances of receiving two or three times as much rainfall than the median rainfall in a single growing season are probably not very high. 2) Excess of water can be detrimental to the production of grain yield. This actually makes sense and can be related to flooding events, plant lodging, increased disease pressure, weaker root anchoring, and even larger number of cloudy days that reduce the amount of solar radiation for photosynthesis. We don’t know the answer from this dataset.\n\nTo complete the analysis we will compute few extra metrics that might be useful to summarize the dataset. I assume most of you are familiar with the concept of median (i.e. 50th percentile), which is auseful metric of central tendency robust to outliers and skewed distributions. Perhaps, the most challenging step is the one regarding the searching of the function roots. We can use the Newton-Raphson method (also known as the secant method) for finding function roots (i.e. at what value(s) of x the function f(x) intersects the x-axis). Based on a visual inspection of the graph, we will pass an initial guess for the serach of 200 mm. If you want to learn more about this function you can read the official SciPy documentation.\nWe can also estimate the ideal rainfall by finding the point at which the frontier does not show any additional increase in yield with increasing rainfall. We can find this optimum point by finding the point at which the first derivative of the frontier function is zero." + }, + { + "objectID": "exercises/frontier_function.html#additional-summary-metrics", + "href": "exercises/frontier_function.html#additional-summary-metrics", + "title": "67  Frontier production functions", + "section": "Additional summary metrics", + "text": "Additional summary metrics\n\n# Median rainfall\nmedian_rainfall = np.median(data.rainfall)\nprint(median_rainfall, 'mm')\n\n# Median grain yield\nmedian_yield = np.median(data.grain_yield)\nprint(median_yield, 'kg/ha')\n\n# Estimate minimum_losses using the Newton-Raphson method\nminimum_losses = newton(cobb_douglas, 200, args=par[0])\nprint('Minimum losses: ', round(minimum_losses), 'mm')\n\n# Optimal rainfall (system input) and grain yield (system output)\n# We will approaximate the first derivative using the set of points (i.e. numerical approx.)\n# Step 1: Calcualte derivative, Step 2: Calculate absolute value, Step 3: find minimum value\nfirst_diff_approax = np.diff(frontier_yield_line)\nidx_zero_diff = np.argmin(np.abs(first_diff_approax))\noptimal_rainfall = frontier_rainfall_line[idx_zero_diff]\noptimal_yield = frontier_yield_line[idx_zero_diff]\nprint('Optimal rainfall:', round(optimal_rainfall), 'mm')\nprint('Optimal yield:', round(optimal_yield), 'kg/ha')\n\n508.254 mm\n1680.25698 kg/ha\nMinimum losses: 147.0 mm\nOptimal rainfall: 550.0 mm\nOptimal yield: 2525.0 kg/ha" + }, + { + "objectID": "exercises/frontier_function.html#quantile-regression", + "href": "exercises/frontier_function.html#quantile-regression", + "title": "67  Frontier production functions", + "section": "Quantile regression", + "text": "Quantile regression\nIn the previous line of reasoning we have focused on selecting the highest yield within a given rainfall interval. This approach makes direct use of both rainfall and yield observations to build the frontier production function.\nAn alternative approach is to calculate some statistical variables for each interval. A commonly used technique is that of selecting percentiles or quantiles. You probably heard the term “Quantile regression analysis”. I’m sure that Python and R packages have some advanced features, but here I want show the concept behind the technique, which is similar to the approach described earlier.\nFor each bin we will simply calculate the yield 95th percentile and the average rainfall. The pairwise points will not necessarily match observations and will likely be smoother since we are filtering out some of the noise.\n\n# Quantile regression\nmax_rainfall = np.max(data.rainfall)\nmin_rainfall = np.min(data.rainfall)\nN = 10 # Number of bins\nrainfall_bins = np.linspace(min_rainfall, max_rainfall, N)\n\nfrontier_yield_obs = np.array([])\nfrontier_rainfall_obs = np.array([])\n\nfor n in range(0,len(rainfall_bins)-1):\n idx = (data.rainfall >= rainfall_bins[n]) & (data.rainfall < rainfall_bins[n+1])\n \n if np.all(idx == False):\n continue\n \n else:\n rainfall_bin = data.loc[idx, 'rainfall']\n yield_bin = data.loc[idx, 'grain_yield'] \n\n frontier_rainfall_obs = np.append(frontier_rainfall_obs, np.mean(rainfall_bin))\n frontier_yield_obs = np.append(frontier_yield_obs, np.percentile(yield_bin, 95))\n\npar0 = [1,1,1]\n\npar = curve_fit(cobb_douglas, frontier_rainfall_obs, frontier_yield_obs, par0)\n\nfrontier_rainfall_line = np.arange(0,max_rainfall)\nfrontier_yield_line = cobb_douglas(frontier_rainfall_line, *par[0])\n\n\n# Plot the data and frontier\nplt.figure(figsize=(8,6))\nplt.scatter(frontier_rainfall_obs, frontier_yield_obs, \n s=200, alpha=0.25, facecolor='b', edgecolors='k', linewidths=1)\nplt.scatter(data[\"rainfall\"], data[\"grain_yield\"], edgecolor='k', alpha=0.75)\nplt.plot(frontier_rainfall_line, frontier_yield_line, '-k')\nplt.xlabel('Growing season precipitation (mm)', size=16)\nplt.ylabel('Grain yield (kg/ha)', size=16)\nplt.xticks(fontsize=16)\nplt.yticks(fontsize=16)\nplt.ylim(0,3200)\nplt.show()\n\n\n\n\n\n# Estimate minimum_losses using the Newton-Raphson method\nminimum_losses = newton(cobb_douglas, 200, args=par[0])\nprint('Minimum losses: ', round(minimum_losses), 'mm')\n\n# Optimal rainfall (system input) and grain yield (system output)\n# We will approaximate the first derivative using the set of points (i.e. numerical approx.)\n# Step 1: Calcualte derivative, Step 2: Calculate absolute value, Step 3: find minimum value\nfirst_diff_approax = np.diff(frontier_yield_line)\nidx_zero_diff = np.argmin(np.abs(first_diff_approax))\noptimal_rainfall = frontier_rainfall_line[idx_zero_diff]\noptimal_yield = frontier_yield_line[idx_zero_diff]\nprint('Optimal rainfall:', round(optimal_rainfall), 'mm')\nprint('Optimal yield:', round(optimal_yield), 'kg/ha')\n\nMinimum losses: 147.0 mm\nOptimal rainfall: 580.0 mm\nOptimal yield: 2592.0 kg/ha" + }, + { + "objectID": "exercises/frontier_function.html#observations", + "href": "exercises/frontier_function.html#observations", + "title": "67  Frontier production functions", + "section": "Observations", + "text": "Observations\nDepending on the method we obtained somewhat different values of minimum losses, optimal rainfall, and optimal yield. So here are some questions for you to think:\n\nAs a researcher how do we determine the right method to analyze our data? Particularly when methods result in slightly different answers.\nRun the code again using a different number of bins. How different are the values for minimum losses and optimum rainfall amounts?\nShould we consider an asymptotic model or a model like the Cobb-Douglas that may exhibit a decreasing trend at high growing season rainfall amounts?" + }, + { + "objectID": "exercises/frontier_function.html#references", + "href": "exercises/frontier_function.html#references", + "title": "67  Frontier production functions", + "section": "References", + "text": "References\nCobb, C.W. and Douglas, P.H., 1928. A theory of production. The American Economic Review, 18(1), pp.139-165.\nFrench, R.J. and Schultz, J.E., 1984. Water use efficiency of wheat in a Mediterranean-type environment. I. The relation between yield, water use and climate. Australian Journal of Agricultural Research, 35(6), pp.743-764.\nGrassini, P., Yang, H. and Cassman, K.G., 2009. Limits to maize productivity in Western Corn-Belt: a simulation analysis for fully irrigated and rainfed conditions. Agricultural and forest meteorology, 149(8), pp.1254-1265.\nPatrignani, A., Lollato, R.P., Ochsner, T.E., Godsey, C.B. and Edwards, J., 2014. Yield gap and production gap of rainfed winter wheat in the southern Great Plains. Agronomy Journal, 106(4), pp.1329-1339." + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#read-and-explore-dataset", + "href": "exercises/atmospheric_carbon_dioxide.html#read-and-explore-dataset", + "title": "68  Atmospheric carbon dioxide", + "section": "Read and explore dataset", + "text": "Read and explore dataset\n\n# Define dataset column names\ncol_names = ['year','month','decimal_date','monthly_avg_co2',\n 'de_seasonalized','days','std','uncertainty']\n\n# Load data\ndf = pd.read_csv('../datasets/co2_mm_mlo.txt', comment='#', delimiter='\\s+', names=col_names)\ndf.head(5)\n\n\n\n\n\n\n\n\nyear\nmonth\ndecimal_date\nmonthly_avg_co2\nde_seasonalized\ndays\nstd\nuncertainty\n\n\n\n\n0\n1958\n3\n1958.2027\n315.70\n314.43\n-1\n-9.99\n-0.99\n\n\n1\n1958\n4\n1958.2877\n317.45\n315.16\n-1\n-9.99\n-0.99\n\n\n2\n1958\n5\n1958.3699\n317.51\n314.71\n-1\n-9.99\n-0.99\n\n\n3\n1958\n6\n1958.4548\n317.24\n315.14\n-1\n-9.99\n-0.99\n\n\n4\n1958\n7\n1958.5370\n315.86\n315.18\n-1\n-9.99\n-0.99\n\n\n\n\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nThe delimiter \\s+ is used when the values in your CSV file are separated by spaces, and you might have more than one consecutive space as a separator. Note that \\s+ is different from \\t, which is used tab-delimited files.\n\n\n\n# Visualize observed data\nplt.figure(figsize=(6,4))\nplt.plot(df['decimal_date'], df['monthly_avg_co2'], '-k')\nplt.title('Mauna Loa, HI')\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.show()" + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#split-dataset-into-train-and-test-sets", + "href": "exercises/atmospheric_carbon_dioxide.html#split-dataset-into-train-and-test-sets", + "title": "68  Atmospheric carbon dioxide", + "section": "Split dataset into train and test sets", + "text": "Split dataset into train and test sets\nThis separation will allow us to fit the model to a larger portion of the dataset and then test it against some unseen data points by the model.\n\n# Split dataset into train and test sets\nidx_train = df['year'] < 2017\ndf_train = df[idx_train]\ndf_test = df[~idx_train]\n\n# Create shorter names for the dependent and independent variables\nstart_date = df_train[\"decimal_date\"][0]\nx_train = df_train[\"decimal_date\"] - start_date # Dates relative to first date\ny_train = df_train['monthly_avg_co2']\n\nx_test = df_test[\"decimal_date\"] - start_date # Dates relative to first date\ny_test = df_test['monthly_avg_co2']\n\n\n\n\n\n\n\nNote\n\n\n\nIn this exercise we are not “training” a model in the machine learning sense. The variables x_train and y_train are used for curve fitting and could have been called x_fit and y_fit." + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#determine-trend-of-time-series", + "href": "exercises/atmospheric_carbon_dioxide.html#determine-trend-of-time-series", + "title": "68  Atmospheric carbon dioxide", + "section": "Determine trend of time series", + "text": "Determine trend of time series\nTo capture the main trend of carbon dioxide concentration over time we will fit the following exponential model:\ny(t) = a + b \\ exp \\bigg(\\frac{c \\ t}{d}\\bigg)\nt is time since March, 1958 (the start of the data set) a, b, c, and d are unknown parameters.\n\n# Define lambda function\nexp_model = lambda t,a,b,c,d: a + b*np.exp(c*t/d)\n\n\n# Fit exponential model\nexp_par = curve_fit(exp_model, x_train, y_train)\n\n# Display parameters\nprint('a:', exp_par[0][0])\nprint('b:', exp_par[0][1])\nprint('c:', exp_par[0][2])\nprint('d:', exp_par[0][3])\n\na: 255.96961561849702\nb: 57.671516523308355\nc: 0.016585403165490224\nd: 1.0298254042563226\n\n\n\n# Predict CO2 concentration using exponential model\ny_train_exp = exp_model(x_train, *exp_par[0])\n\n\n# Define lambda function for the mean absolute error formula\nmae_fn = lambda obs,pred: np.mean(np.abs(obs - pred))\n\n# Compute MAE between observed and predicted carbon dioxide using the expoential model\nmae_train_exp = mae_fn(y_train, y_train_exp)\nprint('MAE using the exponential model is:',np.round(mae_train_exp, 2), 'ppm')\n\nMAE using the exponential model is: 1.87 ppm\n\n\n\n# Overlay obseved and predicted carbon dioxide\nplt.figure(figsize=(6,4))\nplt.plot(df_train['decimal_date'], y_train, '-k', label='Observed')\nplt.plot(df_train['decimal_date'], y_train_exp, '-r', label='Predicted')\nplt.title('Mauna Loa, HI')\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.legend()\nplt.show()\n\n\n\n\n\nExamine residuals of exponential fit\n\n# Compute residuals\nresiduals_exp_fit = y_train - y_train_exp\n\n# Generate scatter plot.\n# We will also add a line plot to better see any temporal trends\nplt.figure(figsize=(10,4))\nplt.scatter(df_train['decimal_date'],residuals_exp_fit, facecolor='w', edgecolor='k')\nplt.plot(df_train['decimal_date'],residuals_exp_fit, color='k')\nplt.title('Residuals')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.show()\n\n\n\n\n\n# Check if residuals approach a zero mean\nprint('Mean residuals (ppm):', np.mean(residuals_exp_fit))\n\nMean residuals (ppm): -4.931054212049981e-06\n\n\nResiduals exhibit a mean close to zero and a sinusoidal pattern. This suggests that a model involving sine or cosine terms could be used to add to improve the exponential model predictions." + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#determine-seasonality-of-time-series", + "href": "exercises/atmospheric_carbon_dioxide.html#determine-seasonality-of-time-series", + "title": "68  Atmospheric carbon dioxide", + "section": "Determine seasonality of time series", + "text": "Determine seasonality of time series\nTo determine the seasonality of the data we will use the de-trended residuals.\ny(t) = A \\ sin(2 \\pi [m + phi] )\nt is time since March, 1958 (the start of the data set) A is amplitude of the wave m is the fractional month (Jan is zero and Dec is 1) phi is the phase constant\n\n# Define the sinusoidal model\nsin_model = lambda t,A,phi: A*np.sin(2*np.pi*((t-np.floor(t)) + phi))\n\n# Fit sinusoidal-exponential model\np0 = [-3, -10]\nsin_par = curve_fit(sin_model, x_train, y_train, p0)\n\n# Display parameters\nprint('A:', sin_par[0][0])\nprint('phi:', sin_par[0][1])\n\nA: 2.9614562555902526\nphi: -9.921208441996372\n\n\n\n# Generate timeseries using sinusoidal-exponential model\ny_train_sin = sin_model(x_train, *sin_par[0])\n\n\n# Visualize residuals of the exponential fit and the fitted sinusoidal model\nplt.figure(figsize=(10,4))\nplt.scatter(df_train['decimal_date'], residuals_exp_fit, facecolor='w', edgecolor='k')\nplt.plot(df_train['decimal_date'], y_train_sin, '-k')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.show()\n\n\n\n\n\n# Close up view for a shorter time span of 50 months\nzoom_range = range(0,50)\nplt.figure(figsize=(10,4))\nplt.scatter(x_train[zoom_range], residuals_exp_fit[zoom_range],\n facecolor='w', edgecolor='k')\nplt.plot(x_train[zoom_range], y_pred_sin[zoom_range], '-k')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.show()\n\n\n\n\nAlthought we can still some minor differences, overall the sinnusoidal model seems to capture the main trend of the residuals. We could compute the residuals of this fit to inspect if there still is a trend that we can exploit to include in our model. In this exercise we will stop here, since this is probably sufficient for most practical applications, but before we move on, let’s plot the residuals of the sinusoidal fit. You will see that slowly the residuals are looking more random.\n\n# Calculate residuals\nresiduals_sin_fit = residuals_exp_fit - y_train_sin\n\n# Plot\nplt.figure(figsize=(10,4))\nplt.scatter(df_train['decimal_date'], residuals_sin_fit, facecolor='w', edgecolor='k')\nplt.title('Residuals')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.show()" + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#combine-trend-and-seasonal-models", + "href": "exercises/atmospheric_carbon_dioxide.html#combine-trend-and-seasonal-models", + "title": "68  Atmospheric carbon dioxide", + "section": "Combine trend and seasonal models", + "text": "Combine trend and seasonal models\nNow that we have an exponential and a sinusoidal model, let’s combine them to have a full deterministic model that we can use to predict and forecast the atmospheric carbon dioxide concentration. The combined model is:\ny(t) = a + b \\ exp \\bigg(\\frac{c \\ t}{d}\\bigg) + A \\ sin(2 \\pi [m + phi] )\n\n# Define the exponential-sinusoidal model\nexp_sin_model = lambda t,a,b,c,d,A,phi: a+b*np.exp(c*t/d) + A*np.sin(2*np.pi*((t-np.floor(t))+phi))\n\n\n# Recall that the parameters for the exponential and sinnusoidal models are:\nprint(exp_par[0])\nprint(sin_par[0])\n\n[2.55969616e+02 5.76715165e+01 1.65854032e-02 1.02982540e+00]\n[ 2.96145626 -9.92120844]\n\n\n\n# So the combined parameters for both models are\nexp_sin_par = np.concatenate((exp_par[0], sin_par[0]))\n\n\n# Predict the time series using the full model\ny_train_exp_sin = exp_sin_model(x_train, *exp_sin_par)\n\n\n# Create figure of combined models\nplt.figure(figsize=(6,4))\nplt.plot(df_train['decimal_date'], y_train, '-k', label='Observed')\nplt.plot(df_train['decimal_date'], y_train_exp_sin, \n color='tomato', alpha=0.75, label='Predicted')\nplt.title('Mauna Loa, HI')\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.legend()\nplt.show()\n\n\n\n\n\n# Compute MAE of combined model against the training set\nmae_train_exp_sin = mae_fn(y_train, y_train_exp_sin)\nprint('MAE using the exponential-sinusoidal model is:',np.round(mae_train_exp_sin, 2), 'ppm')\n\nMAE using the exponential-sinusoidal model is: 1.05 ppm\n\n\n\n# Compute residuals\nresiduals_exp_sin = y_train - y_train_exp_sin\n\n# Plot residuals\nplt.figure(figsize=(6,4))\nplt.scatter(df_train['decimal_date'], residuals_exp_sin, s=10)\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\n\nText(0, 0.5, 'Atmospheric carbon dioxide (ppm)')" + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#full-model-against-test-set", + "href": "exercises/atmospheric_carbon_dioxide.html#full-model-against-test-set", + "title": "68  Atmospheric carbon dioxide", + "section": "Full model against test set", + "text": "Full model against test set\n\n# Predict the time series using the full model\ny_test_exp_sin = exp_sin_model(x_test, *exp_sin_par)\n\n# Compute MAE of combined model against the test set\nmae_test_exp_sin = mae_fn(y_test, y_test_exp_sin)\nprint('MAE using the exponential-sinusoidal model is:',np.round(mae_test_exp_sin, 2), 'ppm')\n\nMAE using the exponential-sinusoidal model is: 1.16 ppm\n\n\n\n# Create figure of combined models\nplt.figure(figsize=(6,4))\nplt.plot(df_test['decimal_date'], y_test, '-k', label='Observed')\nplt.plot(df_test['decimal_date'], y_test_exp_sin, color='tomato', alpha=0.75, label='Predicted')\nplt.title('Mauna Loa, HI')\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#generate-2030-forecast", + "href": "exercises/atmospheric_carbon_dioxide.html#generate-2030-forecast", + "title": "68  Atmospheric carbon dioxide", + "section": "Generate 2030 forecast", + "text": "Generate 2030 forecast\n\n# Forecast of concentration in 2030 (here we only need the relative year value in 2030)\ny_2030 = exp_sin_model(2030 - start_date, *exp_sin_par)\nprint('Carbon dioxide concentration in 2050 is estimated to be:', np.round(y_2030),'ppm')\n\nCarbon dioxide concentration in 2050 is estimated to be: 437.0 ppm\n\n\n\nlast_date = df['decimal_date'].iloc[-1]\nx_forecast = np.arange(last_date, 2030, 0.1) - start_date\ny_forecast = exp_sin_model(x_forecast, *exp_sin_par)\n\n# Figure with projection\nplt.figure(figsize=(6,4))\nplt.plot(df['decimal_date'], df['monthly_avg_co2'], '-k', label='Observed')\nplt.plot(start_date+x_forecast, y_forecast, color='tomato', alpha=0.75, label='Forecast')\nplt.title('Mauna Loa, HI')\nplt.xlabel('Time')\nplt.ylabel('Atmospheric carbon dioxide (ppm)')\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#practice", + "href": "exercises/atmospheric_carbon_dioxide.html#practice", + "title": "68  Atmospheric carbon dioxide", + "section": "Practice", + "text": "Practice\n\nBased on the mean absolute error, what was the error reduction between the model with a trend term alone vs the model with a trend and seasonal terms? Was it worth it? In what situations would you use one or the other?\nAre there any evident trends in the residuals after fitting the sinusoidal model?\nWhat are possible causes of discrepancy between observations of atmospheric carbon dioxide at the Mauna Loa observatory and our best exponential-sinusoidal model?\nTry the same exercise using the Bokeh plotting library, so that you can zoom in and inpsect the fit between the model and observations for specific periods." + }, + { + "objectID": "exercises/atmospheric_carbon_dioxide.html#references", + "href": "exercises/atmospheric_carbon_dioxide.html#references", + "title": "68  Atmospheric carbon dioxide", + "section": "References", + "text": "References\nData source: https://www.esrl.noaa.gov/gmd/ccgg/trends/full.html\nNOAA Greenhouse Gas Marine Boundary Layer Reference: https://www.esrl.noaa.gov/gmd/ccgg/mbl/mbl.html\nMathworks documentation: https://www.mathworks.com/company/newsletters/articles/atmospheric-carbon-dioxide-modeling-and-the-curve-fitting-toolbox.html" + }, + { + "objectID": "exercises/drydowns.html#model-description", + "href": "exercises/drydowns.html#model-description", + "title": "69  Soil moisture drydowns", + "section": "Model description", + "text": "Model description\n SWC = A \\ e^{-t/\\tau} + \\theta_{res}\nSWC = Soil water content in m^{3}/m^{3} A = The initial soil water content m^{3}/m^{3}. Soil water at time t=0 t = Days since rainfall event \\tau = Constant that modulates the rate at which the soil dries \\theta_{res} = Residual soil water content m^{3}/m^{3}.\n\n# Import modules\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom scipy.optimize import curve_fit\nfrom pprint import pprint\n\n\n# Define model using an anonymous lamda function\nmodel = lambda t,tau,A,S_min: A * np.exp(-t/tau) + S_min\nxrange = np.arange(30)\n\n\n# Create figure with example drydowns\nplt.figure(figsize=(5,4))\n\n# Rapid decay. Typical of summer, coarse soils, and actively growing vegetation\nplt.plot(xrange, model(xrange,5,70,50), color='green') \n\n# Drydowns during moderate atmospheric demand (spring and fall)\nplt.plot(xrange, model(xrange,20,70,50), color='tomato')\n\n# Drydown during low atmospheric demand (winter)\nplt.plot(xrange, model(xrange,100,70,50), color='navy')\n\nplt.xlabel('Days since last rainfall')\nplt.ylabel('Storage (mm)')\nplt.show()" + }, + { + "objectID": "exercises/drydowns.html#load-dataset", + "href": "exercises/drydowns.html#load-dataset", + "title": "69  Soil moisture drydowns", + "section": "Load dataset", + "text": "Load dataset\n\n# Load data\ndf = pd.read_csv('../datasets/kings_creek_2022_2023_daily.csv',parse_dates=['datetime'])\ndf.head()\n\n\n\n\n\n\n\n\ndatetime\npressure\ntmin\ntmax\ntavg\nrmin\nrmax\nprcp\nsrad\nwspd\nwdir\nvpd\nvwc_5cm\nvwc_20cm\nvwc_40cm\nsoiltemp_5cm\nsoiltemp_20cm\nsoiltemp_40cm\nbattv\ndischarge\n\n\n\n\n0\n2022-01-01\n96.838\n-14.8\n-4.4\n-9.60\n78.475\n98.012\n0.25\n2.098\n5.483\n0.969\n0.028\n0.257\n0.307\n0.359\n2.996\n5.392\n7.425\n8714.833\n0.0\n\n\n1\n2022-01-02\n97.995\n-20.4\n-7.2\n-13.80\n50.543\n84.936\n0.25\n9.756\n2.216\n2.023\n0.072\n0.256\n0.307\n0.358\n2.562\n4.250\n6.692\n8890.042\n0.0\n\n\n2\n2022-01-03\n97.844\n-9.4\n8.8\n-0.30\n40.622\n82.662\n0.50\n9.681\n2.749\n5.667\n0.262\n0.255\n0.307\n0.358\n2.454\n3.917\n6.208\n8924.833\n0.0\n\n\n3\n2022-01-04\n96.419\n0.1\n8.6\n4.35\n48.326\n69.402\n0.25\n8.379\n5.806\n2.627\n0.363\n0.289\n0.319\n0.357\n2.496\n3.754\n5.842\n8838.292\n0.0\n\n\n4\n2022-01-05\n97.462\n-11.1\n-2.2\n-6.65\n50.341\n76.828\n0.00\n5.717\n4.207\n1.251\n0.126\n0.313\n0.337\n0.357\n1.688\n3.429\n5.567\n8848.083\n0.0\n\n\n\n\n\n\n\n\n# Convert date strings into pandas datetie format\ndf.insert(1, 'doy', df['datetime'].dt.dayofyear)\n\n\n# Compute soil water storage in top 50 cm\ndf['storage'] = df['vwc_5cm']*100 + df['vwc_20cm']*200 + df['vwc_40cm']*200\n\n\n# Plot timeseries of soil moisture and EDDI\nplt.figure(figsize=(8,3))\n\nplt.plot(df['datetime'], df['storage'], color='k', linewidth=1.0)\nplt.ylabel('Soil water storage (mm)')\n\nplt.twinx()\n\nplt.plot(df['datetime'], df['prcp'], color='tomato', linewidth=0.5)\nplt.ylabel('Precipitation (mm)', color='tomato')\n\nplt.show()\n\n\n\n\n\n# Find residual volumetric water content\nstorage_min = df['storage'].min()\nprint(storage_min)\n\n# Define model by forcing minimum storage\nmodel = lambda t,tau,A: A * np.exp(-t/tau) + storage_min\n\n90.80000000000001\n\n\n\n# Iterate over soil moisture timeseries to retrieve drydowns\nday_counter = 0\ndrydown_min_length = 7\nall_drydowns = []\ndrydown_event = {'date':[],'storage':[],'doy':[],\n 'days':[],'length':[], 'par':[]}\n\n# We start the loop on the second day\nfor i in range(1,len(df)):\n delta = df[\"storage\"][i] - df[\"storage\"][i-1]\n \n if delta < 0:\n day_counter += 1\n drydown_event['date'].append(df.loc[i,'datetime'])\n drydown_event['storage'].append(df.loc[i,'storage'])\n drydown_event['doy'].append(df.loc[i,'doy'])\n drydown_event['days'].append(day_counter)\n drydown_event['length'] = day_counter\n\n else:\n # Avoid saving data for short drydowns\n if day_counter < drydown_min_length:\n \n # Reset variables\n day_counter = 0\n drydown_event = {'date':[],'storage':[],'doy':[],\n 'days':[],'length':[], 'par':[]}\n continue\n else:\n \n # Fit model to drydown event\n par_opt, par_cov = curve_fit(model, \n drydown_event['days'], \n drydown_event['storage'])\n drydown_event['par'] = par_opt\n \n # Append current event\n all_drydowns.append(drydown_event)\n \n # Reset variables\n day_counter = 0\n drydown_event = {'date':[],'storage':[],'doy':[],\n 'days':[],'length':[], 'par':[]}\n\n \nprint('There are a total of',len(all_drydowns),'drydowns') \n\nThere are a total of 34 drydowns\n\n\n\n# Inspect one drydown event\npprint(all_drydowns[2])\n\n{'date': [Timestamp('2022-04-12 00:00:00'),\n Timestamp('2022-04-13 00:00:00'),\n Timestamp('2022-04-14 00:00:00'),\n Timestamp('2022-04-15 00:00:00'),\n Timestamp('2022-04-16 00:00:00'),\n Timestamp('2022-04-17 00:00:00'),\n Timestamp('2022-04-18 00:00:00'),\n Timestamp('2022-04-19 00:00:00'),\n Timestamp('2022-04-20 00:00:00')],\n 'days': [1, 2, 3, 4, 5, 6, 7, 8, 9],\n 'doy': [102, 103, 104, 105, 106, 107, 108, 109, 110],\n 'length': 9,\n 'par': array([156.17218773, 76.60354863]),\n 'storage': [167.0,\n 166.4,\n 165.8,\n 165.60000000000002,\n 164.9,\n 164.5,\n 164.1,\n 163.6,\n 163.1]}" + }, + { + "objectID": "exercises/drydowns.html#overlay-soil-moisture-timeseries-and-extracted-drydowns", + "href": "exercises/drydowns.html#overlay-soil-moisture-timeseries-and-extracted-drydowns", + "title": "69  Soil moisture drydowns", + "section": "Overlay soil moisture timeseries and extracted drydowns", + "text": "Overlay soil moisture timeseries and extracted drydowns\n\nplt.figure(figsize=(8,3))\nplt.plot(df['datetime'], df['storage'], color='k', linewidth=1.0)\nfor event in all_drydowns:\n plt.plot(event['date'], model(np.asarray(event['days']), *event['par']), '-r')\n\nplt.ylabel('Soil water storage (mm)')\nplt.show()" + }, + { + "objectID": "exercises/newton_law_cooling.html#practice", + "href": "exercises/newton_law_cooling.html#practice", + "title": "70  Newton’s law of cooling", + "section": "Practice", + "text": "Practice\n\nUsing a cooking thermometer to regularly record the temperature of a hot cup of coffee or te as it cools down. You may want to take readings frequently (e.g. every minute) at the beginning that is when the water cools down rapidly. Use the observations of time and temperature to fit Newton’s law of cooling. Everytime you take a temperature reading also take a sip and take some notes on whether the beverage is too hot, too cold, or at desirable temperature. Using the collected observations, calculate how many minutes you have to drink the coffee/te before it gets too cold." + }, + { + "objectID": "exercises/newton_law_cooling.html#references", + "href": "exercises/newton_law_cooling.html#references", + "title": "70  Newton’s law of cooling", + "section": "References", + "text": "References\nKleiber, M., 1972. A new Newton’s law of cooling?. Science, 178(4067), pp.1283-1285.\nNewton, I., 1701. Scala graduum caloris et frigoris. In Phil. Trans. R. Soc.\nTracy, C.R., 1972. Newton’s law: its application for expressing heat losses from homeotherms. BioScience, 22(11), pp.656-659.\nVollmer, M., 2009. Newton’s law of cooling revisited. European Journal of Physics, 30(5), p.1063." + }, + { + "objectID": "exercises/predator_prey_model.html#equations", + "href": "exercises/predator_prey_model.html#equations", + "title": "71  Predator-Prey model", + "section": "Equations", + "text": "Equations\n\\frac{dx}{dt} = ax - \\beta xy\n\\frac{dy}{dt} = \\delta x y - \\gamma y\nx is the number of prey (for example, rabbits)\ny is the number of some predator (for example, foxes)\nt represents time\n\\frac{dx}{dt} instantaneous growth rate of prey\n\\frac{dy}{dt} instantaneous growth rate of predator\n\\alpha natural prey birth rate in the absence of predation\n\\beta prey death rate due to predation\n\\delta predator birth rate (or efficiency of turning prey flesh into predators, or how many caught prey result into a new predator)\n\\gamma natural predator death rate in the absence of food (prey)\nIf we discretize the equations (meaning that we deal with a finite amount of time instead of an instant), so that dt becomes \\Delta t, then the right hand side of the equations represent change (increment or decrement) of the prey and predator in the specified finite amount of time (e.g. 1 day). So, that:\ndx = (ax - \\beta xy) \\; \\Delta t\nrepresents the change of prey. In order to find the total number of prey at time t we simply need to add this change to the existing quantity of prey:\nx_t = x_{t-1} + (ax_{t-1} - \\beta x_{t-1}y_{t-1}) \\; \\Delta t\n\n# Import modules\nimport matplotlib.pyplot as plt\n\n\n# Initial predator and prey count\npredator = [10]\nprey = [100]\n\n# Prey and predator parameters\nprey_birth_rate = 0.005 # births per day\nprey_death_rate = 0.00015 # deaths per day\npredator_birth_rate = 0.00015 # births per day. If value is too high predators will quickly dominate prey\npredator_death_rate = 0.01 # deaths per day. When there is no food, predators should die quickly, to let the prey recover\n\n# Time parameters\ndelta_time = 1 # 1 day\ntotal_time = 3650 # in days\n\n\n# Run model over defined period\nfor t in range(1, total_time):\n updated_prey = max(prey[t-1] + delta_time * (prey_birth_rate * prey[t-1] - prey_death_rate * prey[t-1] * predator[t-1]), 0) \n updated_predator = max(predator[t-1] + delta_time * (predator_birth_rate * predator[t-1] * prey[t-1] - predator_death_rate * predator[t-1]), 0)\n prey.append(updated_prey)\n predator.append(updated_predator)\n\n\n\n# Create figure\ntime_points = range(total_time)\n\nplt.figure(figsize=(5,4))\nplt.plot(time_points, predator) \nplt.plot(time_points, prey) \nplt.xlabel('Days')\nplt.ylabel('Prey or Predator count')\nplt.legend(['Predator','Prey'])\nplt.rcParams[\"legend.edgecolor\"] = 'w'\nplt.show()" + }, + { + "objectID": "exercises/predator_prey_model.html#references", + "href": "exercises/predator_prey_model.html#references", + "title": "71  Predator-Prey model", + "section": "References", + "text": "References\nLotka, A.J., 1926. Elements of physical biology. Science Progress in the Twentieth Century (1919-1933), 21(82), pp.341-343.\nVolterra, V., 1927. Variazioni e fluttuazioni del numero d’individui in specie animali conviventi (p. 142). C. Ferrari.\nYou can also learn more at https://www.wikiwand.com/en/Lotka%E2%80%93Volterra_equations" + }, + { + "objectID": "exercises/reaction_diffusion.html#references", + "href": "exercises/reaction_diffusion.html#references", + "title": "72  Reaction-Diffusion", + "section": "References", + "text": "References\nHere is an amazing online source that constituted the basis for this code: https://www.karlsims.com/rd.html" + }, + { + "objectID": "exercises/random_walk.html#solution-using-a-for-loop", + "href": "exercises/random_walk.html#solution-using-a-for-loop", + "title": "73  Random walk", + "section": "Solution using a for loop", + "text": "Solution using a for loop\n\n# Set random seed for reproducible results\nnp.random.seed(1)\n\n# Initialize list with particle position\nx = [0]\ny = [0]\n\n# Define number of particle steps\nN = 1000\n\n# Iterate and track the particle over each step\nfor t in range(1,N):\n \n # Generate random step (+1, 0, or -1)\n x_step = np.random.randint(-1,2)\n y_step = np.random.randint(-1,2)\n\n # Update position\n x_new = x[t-1] + x_step\n y_new = y[t-1] + y_step\n \n # Append new position\n x.append(x_new)\n y.append(y_new)\n\n\n# Plot random walk\nplt.figure(figsize=(5,5))\nplt.plot(x, y, markersize=5, linestyle='-', marker='.', color='grey',zorder=0)\nplt.scatter(x[0], y[0], marker='o', facecolor='green', label='Start',zorder=1)\nplt.scatter(x[-1], y[-1], marker='^', facecolor='tomato', label='End', zorder=2)\nplt.xlabel('X coordinate')\nplt.ylabel('Y coordinate')\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/random_walk.html#solution-without-using-a-for-loop", + "href": "exercises/random_walk.html#solution-without-using-a-for-loop", + "title": "73  Random walk", + "section": "Solution without using a for loop", + "text": "Solution without using a for loop\n\n# Set random seed for reproducibility\nnp.random.seed(1)\n\n# Number of particle steps\nN = 1000\n\nx = np.array([0])\ny = np.array([0])\n\n# Generate set of random steps\nx_steps = np.random.randint(-1,2,N)\ny_steps = np.random.randint(-1,2,N)\n\n# Cumulative sum (cumulative effect) of random choices\nx = np.concatenate((x, x_steps)).cumsum()\ny = np.concatenate((y, y_steps)).cumsum()\n\n\n# Plot random walk\nplt.figure(figsize=(5,5))\nplt.plot(x, y, markersize=5, linestyle='-', marker='.', color='grey',zorder=0)\nplt.scatter(x[0], y[0], marker='o', facecolor='green', label='Start',zorder=1)\nplt.scatter(x[-1], y[-1], marker='^', facecolor='tomato', label='End', zorder=2)\nplt.xlabel('X coordinate')\nplt.ylabel('Y coordinate')\nplt.legend()\nplt.show()" + }, + { + "objectID": "exercises/random_walk.html#why-are-the-two-solutions-different", + "href": "exercises/random_walk.html#why-are-the-two-solutions-different", + "title": "73  Random walk", + "section": "Why are the two solutions different?", + "text": "Why are the two solutions different?\nSetting the random seed using np.random.seed(1) ensures that you get the same sequence of random numbers every time you run your code from the start, given that the sequence of random number generation calls is the same.\nThe key point is that the random number generator’s state progresses with each call, so the sequence of numbers you get depends on the number of times you’ve called the generator. The initial seed only sets the starting point of the sequence, but each call to generate a random number advances the state, leading to different subsequent numbers. That’s why the series of numbers generated in the loop and the array generated after the loop are different, despite both sequences being deterministic and reproducible when starting from the same seed.\nHere is some code to visualize the difference in the generated steps:\n\nnp.random.seed(1)\nN = 10\nloop_nums = []\nfor n in range(N):\n loop_nums.append(np.random.randint(-1,2))\n\nprint(loop_nums)\nprint(np.random.randint(-1,2,N))\n\n[0, -1, -1, 0, 0, -1, -1, 0, -1, 0]\n[-1 1 0 1 -1 1 0 1 -1 -1]" + }, + { + "objectID": "exercises/weed_population.html#references", + "href": "exercises/weed_population.html#references", + "title": "74  Weed population model", + "section": "References", + "text": "References\nHolst, N., Rasmussen, I.A. and Bastiaans, L., 2007. Field weed population dynamics: a review of model approaches and applications. Weed Research, 47(1), pp.1-14.\nTrucco, F., Jeschke, M.R., Rayburn, A.L. and Tranel, P.J., 2005. Amaranthus hybridus can be pollinated frequently by A. tuberculatus under field conditions. Heredity, 94(1), pp.64-70." + }, + { + "objectID": "exercises/rainfall_generator.html#read-and-inspect-dataset", + "href": "exercises/rainfall_generator.html#read-and-inspect-dataset", + "title": "75  Rainfall generator", + "section": "Read and inspect dataset", + "text": "Read and inspect dataset\n\n# Load sample data\ndf = pd.read_csv(\"../datasets/KS_Manhattan_6_SSW.csv\",\n na_values=[-9999], parse_dates=['LST_DATE'], date_format='%Y%m%d')\n\n# Inspect a few rows\ndf.head(3)\n\n\n\n\n\n\n\n\nWBANNO\nLST_DATE\nCRX_VN\nLONGITUDE\nLATITUDE\nT_DAILY_MAX\nT_DAILY_MIN\nT_DAILY_MEAN\nT_DAILY_AVG\nP_DAILY_CALC\n...\nSOIL_MOISTURE_5_DAILY\nSOIL_MOISTURE_10_DAILY\nSOIL_MOISTURE_20_DAILY\nSOIL_MOISTURE_50_DAILY\nSOIL_MOISTURE_100_DAILY\nSOIL_TEMP_5_DAILY\nSOIL_TEMP_10_DAILY\nSOIL_TEMP_20_DAILY\nSOIL_TEMP_50_DAILY\nSOIL_TEMP_100_DAILY\n\n\n\n\n0\n53974\n2003-10-01\n1.201\n-96.61\n39.1\nNaN\nNaN\nNaN\nNaN\nNaN\n...\n-99.0\n-99.0\n-99.0\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n1\n53974\n2003-10-02\n1.201\n-96.61\n39.1\n18.9\n2.5\n10.7\n11.7\n0.0\n...\n-99.0\n-99.0\n-99.0\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n2\n53974\n2003-10-03\n1.201\n-96.61\n39.1\n22.6\n8.1\n15.4\n14.8\n0.0\n...\n-99.0\n-99.0\n-99.0\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n\n\n\n\n3 rows × 28 columns\n\n\n\n\n# Add year, month, and day of the year to summarize data in future steps.\ndf[\"YEAR\"] = df[\"LST_DATE\"].dt.year\ndf[\"MONTH\"] = df[\"LST_DATE\"].dt.month\ndf[\"DOY\"] = df[\"LST_DATE\"].dt.dayofyear\ndf.head(3)\n\n\n\n\n\n\n\n\nWBANNO\nLST_DATE\nCRX_VN\nLONGITUDE\nLATITUDE\nT_DAILY_MAX\nT_DAILY_MIN\nT_DAILY_MEAN\nT_DAILY_AVG\nP_DAILY_CALC\n...\nSOIL_MOISTURE_50_DAILY\nSOIL_MOISTURE_100_DAILY\nSOIL_TEMP_5_DAILY\nSOIL_TEMP_10_DAILY\nSOIL_TEMP_20_DAILY\nSOIL_TEMP_50_DAILY\nSOIL_TEMP_100_DAILY\nYEAR\nMONTH\nDOY\n\n\n\n\n0\n53974\n2003-10-01\n1.201\n-96.61\n39.1\nNaN\nNaN\nNaN\nNaN\nNaN\n...\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n2003\n10\n274\n\n\n1\n53974\n2003-10-02\n1.201\n-96.61\n39.1\n18.9\n2.5\n10.7\n11.7\n0.0\n...\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n2003\n10\n275\n\n\n2\n53974\n2003-10-03\n1.201\n-96.61\n39.1\n22.6\n8.1\n15.4\n14.8\n0.0\n...\n-99\n-99\nNaN\nNaN\nNaN\nNaN\nNaN\n2003\n10\n276\n\n\n\n\n3 rows × 31 columns\n\n\n\n\n# Observe trends in precipitation data (2004 to 2016)\nplt.figure(figsize=(5,4))\nplt.xlim(0,400)\nplt.xlabel('Day of the year')\nplt.ylabel('Precipitation (mm)')\nfor year in range(2004,2017):\n idx_year = df[\"YEAR\"] == year\n P_cum = df[\"P_DAILY_CALC\"][idx_year].cumsum()\n plt.plot(range(1,len(P_cum)+1), P_cum)\n \n\n\n\n\n\n# Observe histograms of precipitation\n\nplt.figure(figsize=(5,4))\nplt.xlabel('Annual precipitation (mm)')\nplt.ylabel('Density')\nfor year in range(2004,2017):\n idx_year = df[\"YEAR\"] == year\n rainfall_year = df[\"P_DAILY_CALC\"][idx_year]\n rainfall_year = rainfall_year[rainfall_year > 1]\n plt.hist(rainfall_year, bins=25, density=True)" + }, + { + "objectID": "exercises/rainfall_generator.html#modeling-rainfall-as-a-markov-chain.", + "href": "exercises/rainfall_generator.html#modeling-rainfall-as-a-markov-chain.", + "title": "75  Rainfall generator", + "section": "Modeling rainfall as a Markov chain.", + "text": "Modeling rainfall as a Markov chain.\nThis type of rainfall generators is kwnon as the Richardson-type models (Richardson and Wright, 1984). The model computes the rainfall at time t as a function of time t-1 in three steps. To be consistent with the original manuscript by Richardson and Wright (1984), we will use the term “wet” for a day with measurable rainfall and “dry” for a day without measurable rainfall.\nStep 1: Find if the previous day was dry or wet\nStep 2: Compute the probability of having a rainfall event on day t given the condition on the previous day. So, here we need to have two different probability distributions, one that we will use if yesterday was dry and another if yesterday was wet.\n\nWhat is the probability of having a wet day if yesterday was dry? (i.e. P(W/D) )\nWhat is the probability of having a wet day if yesterday was wet? (i.e. P(W/W) )\n\nTo accurately simulate seasonal rainfall trends, the model relies on Wet-Dry and Wet-Wet distributions on a monthly basis.\nSteps 1 and 2 deal with the probability of rainfall occurrence, not the amount of rainfall. The next step will help us determine the amout of rainfall on wet days.\nStep 3: If as a consequence of the random process we obtain that on day t there is a rainfall event (i.e., day t is a wet day), then we have to compute the amount. To do this we will use a gamma distribution, which is suited to heavily skewed distributions, such as those resulting from histograms of daily rainfall (see previous histogram figure). To complete this step were need to have some parameters that describe this rainfall amount distribution on a monthly basis. These parameters are often determined by fitting the gamma distribution to historical rainfall data.\nWe will first run an example using the loaded dataset to learn how to compute the Wet-Dry and Wet-Wet probabilities for the entire year. It’s important that we first learn how to compute the simplest step before moving into a more detailed characterization of the rainfall process on a monthly basis." + }, + { + "objectID": "exercises/rainfall_generator.html#determine-probability-of-rainfall-occurrence-by-month", + "href": "exercises/rainfall_generator.html#determine-probability-of-rainfall-occurrence-by-month", + "title": "75  Rainfall generator", + "section": "Determine probability of rainfall occurrence by month", + "text": "Determine probability of rainfall occurrence by month\nThe first step of this process is to characterize the probability functions using observed data.\n\nProbability of Wet-Dry and Wet-Wet days\n\n# Initialize empty arrays\nWW = []\nWD = []\nmonth_WW = []\nmonth_WD = []\n\n# Create shorter variable names\nmonths = df[\"MONTH\"].values\nrainfall = df[\"P_DAILY_CALC\"].values\n\nfor i in range(1,df.shape[0]):\n \n # Determine if yesterday was Wet or Dry\n # Append \"1\" if today was Wet, else append \"0\"\n if rainfall[i-1] > 0:\n \n month_WW.append(months[i])\n if rainfall[i] > 0:\n WW.append(1)\n else:\n WW.append(0)\n \n elif rainfall[i-1] == 0:\n \n month_WD.append(months[i])\n if rainfall[i] > 0:\n WD.append(1)\n else:\n WD.append(0)\n\nprint(sum(WW)/len(WW))\nprint(sum(WD)/len(WD))\n\n0.4166666666666667\n0.20590604026845638\n\n\n\n\nCreate table with probabilities by month\n\n# Wet/Wet table\ndf_WW = pd.DataFrame({'month':month_WW, 'WW':WW})\n\n# Wet/Dry\ndf_WD = pd.DataFrame({'month':month_WD, 'WD':WD})\n\n# Compute monthly probabilities per month\nmonthly_WW = df_WW.groupby(\"month\").sum() / df_WW.groupby(\"month\").count()\nmonthly_WD = df_WD.groupby(\"month\").sum() / df_WD.groupby(\"month\").count()\n\n# Create single table of parameters\ncoeff = pd.concat([monthly_WW, monthly_WD], axis=1)\ncoeff\n\n\n\n\n\n\n\n\nWW\nWD\n\n\nmonth\n\n\n\n\n\n\n1\n0.338028\n0.141176\n\n\n2\n0.320513\n0.161184\n\n\n3\n0.401709\n0.219048\n\n\n4\n0.503448\n0.273063\n\n\n5\n0.480769\n0.290909\n\n\n6\n0.442953\n0.293680\n\n\n7\n0.415254\n0.228571\n\n\n8\n0.412214\n0.245847\n\n\n9\n0.362745\n0.201258\n\n\n10\n0.409524\n0.193353\n\n\n11\n0.388889\n0.129794\n\n\n12\n0.381579\n0.144092" + }, + { + "objectID": "exercises/rainfall_generator.html#determine-probability-of-daily-rainfall-amount", + "href": "exercises/rainfall_generator.html#determine-probability-of-daily-rainfall-amount", + "title": "75  Rainfall generator", + "section": "Determine probability of daily rainfall amount", + "text": "Determine probability of daily rainfall amount\n\n# Fit gamma distribution to each month\nmonthly_shape = []\nmonthly_scale = []\n\nfor m in range(1,13):\n idx_month = df[\"MONTH\"] == m\n x = df[\"P_DAILY_CALC\"].loc[idx_month]\n x = x[~np.isnan(x)] # Save only non NAN values\n x = x[x>0] # Save only positive rainfall values\n shape, loc, scale = gamma.fit(x, floc=0)\n monthly_shape.append(shape)\n monthly_scale.append(scale)\n\n# Append amount parameters to monthly lookup table\ncoeff[\"shape\"] = monthly_shape\ncoeff[\"scale\"] = monthly_scale\ncoeff.head(12)\n\n\n\n\n\n\n\n\nWW\nWD\nshape\nscale\n\n\nmonth\n\n\n\n\n\n\n\n\n1\n0.338028\n0.141176\n0.748257\n4.166760\n\n\n2\n0.320513\n0.161184\n0.678656\n8.142081\n\n\n3\n0.401709\n0.219048\n0.632519\n11.092325\n\n\n4\n0.503448\n0.273063\n0.589697\n15.483557\n\n\n5\n0.480769\n0.290909\n0.630033\n13.661336\n\n\n6\n0.442953\n0.293680\n0.599585\n22.291664\n\n\n7\n0.415254\n0.228571\n0.677182\n16.648971\n\n\n8\n0.412214\n0.245847\n0.625811\n21.885339\n\n\n9\n0.362745\n0.201258\n0.585308\n15.593047\n\n\n10\n0.409524\n0.193353\n0.659315\n11.230858\n\n\n11\n0.388889\n0.129794\n0.628394\n10.430029\n\n\n12\n0.381579\n0.144092\n0.668791\n9.900732\n\n\n\n\n\n\n\nCheck that our gamma function can generate sound rainfall distributions. Note that the figure below is the probability density function (pdf), not the actual rainfall amount. The y-axis contains density information while the x-axis contains rainfall data.\n\n# Show frequency of rainfall amount for last month of the fitting\na,b,c = gamma.fit(x, floc=0)\n\nplt.figure(figsize=(5,4))\nplt.hist(x, bins='scott', density=True)\nrainfall_range = np.linspace(1, 50, 1000)\nplt.plot(rainfall_range, gamma.pdf(rainfall_range,a,b,c))\nplt.xlabel('Daily precipitation (mm)')\nplt.ylabel('Density')\nplt.show()" + }, + { + "objectID": "exercises/rainfall_generator.html#build-rainfall-generator", + "href": "exercises/rainfall_generator.html#build-rainfall-generator", + "title": "75  Rainfall generator", + "section": "Build rainfall generator", + "text": "Build rainfall generator\nNow that we have all the distribution properties we can implement the rainfall generator.\n\n# Define rainfall simulator\ndef rainfall_gen(dates,coeff):\n \n P = np.ones(dates.shape[0])*np.nan\n P[0] = 0\n\n for t in range(1,dates.shape[0]):\n month = dates.month[t]\n if P[t-1] == 0:\n if np.random.rand() > coeff[\"WD\"][month]:\n P[t] = 0\n else:\n P[t] = gamma.rvs(coeff[\"shape\"][month],0,coeff[\"scale\"][month])\n P[t] = np.round(P[t]*10)/10\n\n elif P[t-1] > 0:\n if np.random.rand() > coeff[\"WW\"][month]:\n P[t] = 0\n else:\n P[t] = gamma.rvs(coeff[\"shape\"][month],0,coeff[\"scale\"][month])\n P[t] = np.round(P[t]*10)/10\n\n P_total = P.sum()\n return P" + }, + { + "objectID": "exercises/rainfall_generator.html#create-rainfall-scenarios-with-the-generator", + "href": "exercises/rainfall_generator.html#create-rainfall-scenarios-with-the-generator", + "title": "75  Rainfall generator", + "section": "Create rainfall scenarios with the generator", + "text": "Create rainfall scenarios with the generator\n\n# Create an example set of dates\ndates = pd.date_range(\"2018-01-01\",\"2018-12-31\",freq=\"D\")\n\n# Call rainfall generator multiple times and plot cumulative rainfall\nplt.figure(figsize=(5,4))\nfor i in range(20):\n P = rainfall_gen(dates,coeff)\n plt.plot(P.cumsum())\n \nplt.title(\"Rainfall generator\")\nplt.xlabel(\"DOY\")\nplt.ylabel(\"Rainfall amount (mm)\")\nplt.show()\n\n\n\n\n\n# Plot the last iteration\nplt.figure(figsize=(10,3))\nplt.bar(dates,P)\nplt.title(\"Daily rainfall\") # for last year of the previous simulation\nplt.ylabel(\"Rainfall amount (mm)\")\nplt.show()" + }, + { + "objectID": "exercises/rainfall_generator.html#references", + "href": "exercises/rainfall_generator.html#references", + "title": "75  Rainfall generator", + "section": "References", + "text": "References\nIntergernmental Panel on Climate Change Data Distribution Centre: https://www.ipcc-data.org/guidelines/pages/weather_generators.html\nJones, P., Harpham, C., Kilsby, C., Glenis, V. and Burton, A., 2010. UK Climate Projections science report: Projections of future daily climate for the UK from the Weather Generator.\nKilsby, C.G., Jones, P.D., Burton, A., Ford, A.C., Fowler, H.J., Harpham, C., James, P., Smith, A. and Wilby, R.L., 2007. A daily weather generator for use in climate change studies. Environmental Modelling & Software, 22(12), pp.1705-1719.\nRichardson, C.W., 1982. Dependence structure of daily temperature and solar radiation. Transactions of the ASAE, 25(3), pp.735-0739.\nRichardson, C.W. and Wright, D.A., 1984. WGEN: A model for generating daily weather variables." + }, + { + "objectID": "exercises/count_seeds.html", + "href": "exercises/count_seeds.html", + "title": "76  Count seeds", + "section": "", + "text": "In this exercise we will learn how to use image analysis to identify and count seeds from an image collected with a mobile device.\n\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\n\nfrom skimage.filters import threshold_otsu\nfrom skimage.morphology import area_opening, disk, binary_closing\nfrom skimage.measure import find_contours, label\nfrom skimage.color import rgb2gray, label2rgb\n\n\n# Read color image\nimage_rgb = mpimg.imread('../datasets/images/rice_seeds.jpg')\n\n\n# Convert image to grayscale\nimage_gray = rgb2gray(image_rgb)\n\n\n# Visualize rgb and grayscale images\nplt.figure(figsize=(8,4))\n\nplt.subplot(1,2,1)\nplt.imshow(image_rgb)\nplt.axis('off')\nplt.title('RGB')\nplt.tight_layout()\n\nplt.subplot(1,2,2)\nplt.imshow(image_gray, cmap='gray')\nplt.axis('off')\nplt.title('Gray scale')\nplt.tight_layout()\n\nplt.show()\n\n\n\n\n\n# Segment seeds using a global automated threshold\nglobal_thresh = threshold_otsu(image_gray)\nimage_binary = image_gray > global_thresh\n\n\n# Display classified seeds and grayscale threshold\nplt.figure(figsize=(12,4))\n\nplt.subplot(1,3,1)\nplt.imshow(image_gray, cmap='gray')\nplt.axis('off')\nplt.title('Original grayscale')\nplt.tight_layout()\n\nplt.subplot(1,3,2)\nplt.hist(image_gray.ravel(), bins=256)\nplt.axvline(global_thresh, color='r', linestyle='--')\nplt.title('Otsu threshold')\nplt.xlabel('Grayscale')\nplt.ylabel('Counts')\n\nplt.subplot(1,3,3)\nplt.imshow(image_binary, cmap='gray')\nplt.axis('off')\nplt.title('Binary')\nplt.tight_layout()\n\nplt.show()\n\n\n\n\n\n# Invert image\nimage_binary = ~image_binary\n\n\n# Remove small areas (remove noise)\nimage_binary = area_opening(image_binary, area_threshold=1000, connectivity=2)\n\n\n# Closing (performs a dilation followed by an erosion. Connect small bright patches)\nimage_binary = binary_closing(image_binary, disk(5))\n\n# Let's inspect the structuring element\nprint(disk(5))\n\n[[0 0 0 0 0 1 0 0 0 0 0]\n [0 0 1 1 1 1 1 1 1 0 0]\n [0 1 1 1 1 1 1 1 1 1 0]\n [0 1 1 1 1 1 1 1 1 1 0]\n [0 1 1 1 1 1 1 1 1 1 0]\n [1 1 1 1 1 1 1 1 1 1 1]\n [0 1 1 1 1 1 1 1 1 1 0]\n [0 1 1 1 1 1 1 1 1 1 0]\n [0 1 1 1 1 1 1 1 1 1 0]\n [0 0 1 1 1 1 1 1 1 0 0]\n [0 0 0 0 0 1 0 0 0 0 0]]\n\n\n\n# Display inverted and denoised binary image\nplt.figure(figsize=(4,4))\n\nplt.imshow(image_binary, cmap='gray')\nplt.axis('off')\nplt.title('Binary')\nplt.tight_layout()\n\nplt.show()\n\n\n\n\n\n# Identify seed boundaries\ncontours = find_contours(image_binary, 0)\n\n# Print number of seeds in image\nprint('Image contains',len(contours),'seeds')\n\nImage contains 41 seeds\n\n\n\n# Plot seed contours\nplt.figure(figsize=(4,4))\nplt.imshow(image_binary, cmap='gray')\nplt.axis('off')\nplt.tight_layout()\n\nfor contour in contours:\n plt.plot(contour[:, 1], contour[:, 0], '-r', linewidth=1.5)\n \n\n\n\n\n\n# Label image regions\nlabel_image = label(image_binary)\nimage_label_overlay = label2rgb(label_image, image=image_binary)\n\n\n# Display image regions on top of original image\nplt.figure(figsize=(4, 4))\nplt.imshow(image_label_overlay)\nplt.tight_layout()\nplt.axis('off')\nplt.show()\n\n\n\n\n\n# Display contour for a single seed\nplt.figure(figsize=(12, 8))\n\nfor seed in range(36):\n plt.subplot(6,6,seed+1)\n plt.plot(contours[seed][:, 1], contours[seed][:, 0], '-r', linewidth=2)\n plt.tight_layout()\nplt.show()" + }, + { + "objectID": "exercises/canopy_cover.html#read-and-process-a-single-image", + "href": "exercises/canopy_cover.html#read-and-process-a-single-image", + "title": "77  Canopy cover", + "section": "Read and process a single image", + "text": "Read and process a single image\n\n# Read example image\nrgb = mpimg.imread('../datasets/images/grassland.jpg')\n\n\n# Display image\nplt.imshow(rgb)\nplt.axis('off')\nplt.show()\n\n\n\n\n\n# Inspect shape\nprint(rgb.shape)\nprint(rgb.dtype)\n\n(512, 512, 3)\nfloat32\n\n\nImages are often represented as unsigned integers of 8 bits. This means that each pixel in each band can only hold one of 256 integer values. Because the range is zero-index, the pixel values can range from 0 to 255. The color of a pixel is repreented by triplet, for example the triplet (0,0,0) represents black, while (255,255,255) represents white. Similarly, the triplet (255,0,0) represents red and (255,220,75) represents a shade of yellow.\n\n# Extract data in separate variable for easier manipulation.\nred = rgb[:, :, 0] #Extract matrix of red pixel values (m by n matrix)\ngreen = rgb[:, :, 1] #Extract matrix of green pixel values\nblue = rgb[:, :, 2] #Extract matrix of blue pixel values\n\n\n# Compare shape with original image\nprint(red.shape)\n\n(512, 512)\n\n\n\n# Show information in single bands\nplt.figure(figsize=(12,8))\n\nplt.subplot(2,3,1)\nplt.imshow(red, cmap=\"gray\")\nplt.axis('off')\n\nplt.subplot(2,3,2)\nplt.imshow(green, cmap=\"gray\")\nplt.axis('off')\n\nplt.subplot(2,3,3)\nplt.imshow(blue, cmap=\"gray\")\nplt.axis('off')\n\n# Add histograms using Doane's rule for histogram bins\nplt.subplot(2,3,4)\nplt.hist(red.flatten(), bins='doane')\n\nplt.subplot(2,3,5)\nplt.hist(green.flatten(), bins='doane')\n\nplt.subplot(2,3,6)\nplt.hist(blue.flatten(), bins='doane')\nplt.show()\n\n\n\n\nFind out more about all the different methods for generating hsitogram bins here\n\n# Calculate red to green ratio for each pixel. The result is an m x n array.\nred_green_ratio = red/green\n\n# Calculate blue to green ratio for each pixel. The result is an m x n array.\nblue_green_ratio = blue/green\n\n# Excess green\nExG = 2*green - red - blue\n\n\n# Let's check the resulting data type of the previous computation\nprint(red_green_ratio.shape)\nprint(blue_green_ratio.dtype)\n\n(512, 512)\nfloat32\n\n\nThe size of the array remains unchanged, but Python automatically changes the data type from uint8 to float64. This is great because we need to make use of a continuous numerical scale to classify our green pixels. By generating the color ratios our scale also changes. Let’s look a this using a histogram.\n\n# Plot histogram\nplt.figure()\nplt.hist(red_green_ratio.flatten(), bins='scott')\nplt.xlim(0.5,1.5)\nplt.show()\n\n\n\n\n\n# Classification of green pixels\nbw = np.logical_and(red_green_ratio<0.95, blue_green_ratio<0.95, ExG>20) \n\n\nprint(bw.shape)\nprint(bw.dtype)\nprint(bw.size)\n\n(512, 512)\nbool\n262144\n\n\nSee that we started with an m x n x 3 (original image) and we finished with and m x n x 2 (binary or classified image)\n\n# Compute percent green canopy cover\ncanopy_cover = np.sum(bw) / np.size(bw) * 100 \nprint('Green canopy cover:',round(canopy_cover,2),' %')\n\nGreen canopy cover: 56.64 %\n\n\n\nplt.figure(figsize=(12,4))\n\n# Original image\nplt.subplot(1, 2, 1)\nplt.imshow(rgb)\nplt.title('Original')\nplt.axis('off')\n\n# CLassified image\nplt.subplot(1, 2, 2)\nplt.imshow(bw, cmap='gray')\nplt.title('Classified')\nplt.axis('off')\n\nplt.show()\n\n\n\n\nClassified pixels are displayed in white. The classification for this image is exceptional due to the high contrast between the plant and the background. There are also small regions where our appraoch misclassified grren canopy cover as a consequence of bright spots on the leaves. For many applications this error is small and can be ignored, but this issue highlights the importance of taking high quality pictures in the field.\n\n\n\n\n\n\nTip\n\n\n\nIf possible, take your time to collect high-quality and consistent images. Effective image analysis starts with high quality images." + }, + { + "objectID": "exercises/canopy_cover.html#references", + "href": "exercises/canopy_cover.html#references", + "title": "77  Canopy cover", + "section": "References", + "text": "References\nPatrignani, A. and Ochsner, T.E., 2015. Canopeo: A powerful new tool for measuring fractional green canopy cover. Agronomy Journal, 107(6), pp.2312-2320." + }, + { + "objectID": "exercises/lakes_kansas.html#save-image-with-identified-lakes", + "href": "exercises/lakes_kansas.html#save-image-with-identified-lakes", + "title": "78  Find Kansas lakes", + "section": "Save image with identified lakes", + "text": "Save image with identified lakes\n\n# Save resulting BW array as a raster map\nprofile = raster.profile\nprofile.update(count=1)\nwith rasterio.open('lakes.tiff', 'w', **profile) as f:\n f.write(BW, 1)\n\n\n# Read the lakes GeoTiff back into Python and display map (noticed that now the images has coordinates)\nlakes = rasterio.open('lakes.tiff')\nshow(lakes, cmap='gray')\nplt.show()" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#load-base-maps", + "href": "exercises/soil_moisture_monitoring_stations.html#load-base-maps", + "title": "79  Soil moisture monitoring stations", + "section": "Load base maps", + "text": "Load base maps\n\n# Define non-continental territories\nnon_contiguous_territories = ['Alaska','Hawaii','Puerto Rico','American Samoa','United States Virgin Islands','Guam','Commonwealth of the Northern Mariana Islands']\n\n# Read US counties\ncounties = gpd.read_file('../datasets/spatial/us_county_5m.geojson')\nidx = counties['STATE_NAME'].isin(non_contiguous_territories)\ncounties = counties[~idx].reset_index(drop=True)\n\n\n# Read US states\nstates = gpd.read_file('../datasets/spatial/us_state_5m.geojson')\nidx = states['NAME'].isin(non_contiguous_territories)\nstates = states[~idx].reset_index(drop=True)" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#load-stations-dataset", + "href": "exercises/soil_moisture_monitoring_stations.html#load-stations-dataset", + "title": "79  Soil moisture monitoring stations", + "section": "Load stations dataset", + "text": "Load stations dataset\n\n# Load station data\n\n# Read file of soil moisture monitoring networks in North America\ndf_stations = pd.read_csv('../datasets/spatial/usa_soil_moisture_stations.csv',\n skiprows=[0])\n\n# Geodataframe\nstations = gpd.GeoDataFrame(df_stations,\n geometry=gpd.points_from_xy(df_stations['longitude'],df_stations['latitude']), \n crs=\"EPSG:4326\")\n\n# For convenience sort stations by network and reset index\nstations.sort_values(by='network', ascending=True, inplace=True)\nstations.reset_index(drop=True, inplace=True)\n\n# Display a few rows of the new GeoDataframe\nstations.head(3)\n\n\n\n\n\n\n\n\nstation\nnetwork\nlatitude\nlongitude\nsource\ngeometry\n\n\n\n\n0\nAttawapiskat River Bog\nAmeriFlux\n52.6950\n-83.9452\nAMERIFLUX\nPOINT (-83.94520 52.69500)\n\n\n1\nButte County Rice Farm\nAmeriFlux\n39.5782\n-121.8579\nAMERIFLUX\nPOINT (-121.85790 39.57820)\n\n\n2\nArkansas Corn Farm\nAmeriFlux\n34.4159\n-91.6733\nAMERIFLUX\nPOINT (-91.67330 34.41590)" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#clip-stations-to-counties", + "href": "exercises/soil_moisture_monitoring_stations.html#clip-stations-to-counties", + "title": "79  Soil moisture monitoring stations", + "section": "Clip stations to counties", + "text": "Clip stations to counties\nSince several stations are located outside of the contiguous U.S., we need to clip the stations dataset to the counties dataset (which was already contrained to the contiguous U.S. when we imported the base maps).\n\n# Clip stations to counties\nstations = stations.clip(counties)" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#find-counties-with-stations", + "href": "exercises/soil_moisture_monitoring_stations.html#find-counties-with-stations", + "title": "79  Soil moisture monitoring stations", + "section": "Find counties with stations", + "text": "Find counties with stations\nIn order to represent soil moisture conditions across the country, some national programs and researchers are considering the goal of establishing at least one monitoring station per county. So, how many counties have at least one station of our dataset?\nGeoPandas has built-in functions that will make this analysis straight forward. However, one-line solutions sometimes feel like a black box, so there are other alternatives using basic building blocks like for loops and if statements that I would also like to consider.\n\n# One-line solution using an intersection of counties and the union of all stations \ncounties['has_station'] = counties.intersects(stations.unary_union)\ncounties.head(3)\n\n\n\n\n\n\n\n\nSTATEFP\nCOUNTYFP\nCOUNTYNS\nAFFGEOID\nGEOID\nNAME\nNAMELSAD\nSTUSPS\nSTATE_NAME\nLSAD\nALAND\nAWATER\ngeometry\nhas_station\n\n\n\n\n0\n13\n233\n00343585\n0500000US13233\n13233\nPolk\nPolk County\nGA\nGeorgia\n06\n803775591\n4664760\nPOLYGON ((-85.42188 34.08082, -85.28332 34.079...\nFalse\n\n\n1\n21\n023\n00516858\n0500000US21023\n21023\nBracken\nBracken County\nKY\nKentucky\n06\n524900457\n16279752\nPOLYGON ((-84.23042 38.82740, -84.23018 38.826...\nFalse\n\n\n2\n28\n153\n00695797\n0500000US28153\n28153\nWayne\nWayne County\nMS\nMississippi\n06\n2099745602\n7255476\nPOLYGON ((-88.94296 31.56566, -88.94272 31.607...\nFalse\n\n\n\n\n\n\n\n\n\n\n\n\n\nNote\n\n\n\nThe union of all station geometries creates a single multipoint geometry, that then can be used to intersect each county polygon. To visualize this multipoint geometry, run in a cell the following command: print(stations.unary_union)\n\n\nAlternative code\nThe following code achieves the same goal as the previous one-line solution. The code is longer, but it was solved using basic Python control flow statements like for loops, if statements, and booleans.\n# Create empty boolean matching the number of counties\ncontains_station = np.full(counties.shape[0], False)\n\n# Iterate over each county and check if contains at least one station\nfor k,c in counties.iterrows():\n if sum(c['geometry'].contains(stations['geometry'])) > 0:\n contains_station[k] = True\n \n# Add boolean array as a new column into counties GeoDataframe\ncounties['has_station'] = contains_station\n\n# Count number of counties with stations \nN_counties = counties['has_station'].sum()\nprint(f'Counties with at least one station: {N_counties}')\n\n# Calculate percentage of all counties in the contiguous U.S.\nN_counties_perc = round(counties['has_station'].sum()/counties.shape[0]*100)\nprint(f'Percent of all counties with at least one station: {N_counties_perc}')\n\n# Count number of unique networks\nN_networks = len(stations['network'].unique())\nprint(f'There are: {N_networks} unique monitoring networks')\n\nCounties with at least one station: 963\nPercent of all counties with at least one station: 31\nThere are: 24 unique monitoring networks" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#create-map", + "href": "exercises/soil_moisture_monitoring_stations.html#create-map", + "title": "79  Soil moisture monitoring stations", + "section": "Create map", + "text": "Create map\n\n# Define colormap\ncmap = plt.colormaps['Paired']\n\n# Define markers (multiply by 10 to have more markers than networks)\nmarkers = ['o','s','d','v'] * 10\n\n# Create base map (counties)\nbase = counties.plot(color='none', edgecolor=\"lightgray\",figsize=(20,20))\n\n# Add state boundaries to base map\nstates.plot(ax=base, color=\"none\", edgecolor=\"black\", linewidth=1.5)\n\n# Uncomment line below to add scale bar\n# base.add_artist(ScaleBar(1)) \n\n# Add stations from each network using a loop to add \n# network-specific styling\nfor k, network in enumerate(stations['network'].unique()):\n idx = stations['network'] == network\n gdf_value = stations[idx]\n color = cmap(k/N_networks)\n gdf_value.plot(ax=base, marker=markers[k], label=network, markersize=50, alpha=1.0, edgecolor='k', aspect=1.25)\n \nplt.title('U.S. Mesoscale Environmental Monitoring Networks with In Situ Soil Moisture Observations', size=20, fontweight='bold')\nplt.xlabel('Longitude', size=20)\nplt.ylabel('Latitude', size=20)\nplt.xticks(fontsize=20)\nplt.yticks(fontsize=20)\nplt.text(-125, 27.5, f\"Number of networks: {N_networks}\", size=20)\nplt.text(-125, 26, f\"Number of stations: {stations.shape[0]}\", size=20)\nplt.text(-125, 24.5, f\"Number of counties with at least one station: {N_counties} ({N_counties_perc}%)\", size=20)\nplt.legend(loc='best', ncol=1, fontsize=16, bbox_to_anchor=(1, 1.015))\n\n# # Uncomment line below to place legend outside (and mute previous line)\n# plt.legend(loc='lower left', ncol=5, fontsize=15, bbox_to_anchor=(0, -0.35))\n\n# Uncomment line to save figure\n# plt.savefig('us_map_soil_moisture_stations_2023.jpg', dpi=600, bbox_inches='tight')\n\nplt.show()" + }, + { + "objectID": "exercises/soil_moisture_monitoring_stations.html#practice", + "href": "exercises/soil_moisture_monitoring_stations.html#practice", + "title": "79  Soil moisture monitoring stations", + "section": "Practice", + "text": "Practice\n\nCreate separate maps for other U.S. territories and add them to the previous either as subplots or as inset maps." + }, + { + "objectID": "exercises/field_random_samples.html#test-if-point-is-inside-watershed", + "href": "exercises/field_random_samples.html#test-if-point-is-inside-watershed", + "title": "80  Field random samples", + "section": "Test if point is inside watershed", + "text": "Test if point is inside watershed\n\n# Create a point using Shapely\np = Point(-96.560, 39.092) # Intensionally inside the watershed\n\n# Create a polygon of the watershed using Shapely\n# We will make use of the itertuples method to conver a Pandas row into a tuple\ncoords = list(df.itertuples(index=False, name=None))\nwatershed = Polygon(coords)\n\n# Check if the point is inside polygon\np.within(watershed)\n\nTrue\n\n\n\n# Access point coordinates\nprint(f'x:{p.x} and y:{p.y}')\n\nx:-96.56 and y:39.092\n\n\n\n# Confirm visually\nlon_watershed,lat_watershed = watershed.boundary.xy\n\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\nplt.scatter(p.x,p.y, marker='x', color='r')\nplt.axis('equal')\nplt.show()" + }, + { + "objectID": "exercises/field_random_samples.html#generate-random-sampling-points", + "href": "exercises/field_random_samples.html#generate-random-sampling-points", + "title": "80  Field random samples", + "section": "Generate random sampling points", + "text": "Generate random sampling points\nLet’s generate a set of N random sampling points within the watershed.\n\n# Let's make use of the watershed bounds for our points\n# bounding box is a (minx, miny, maxx, maxy) \nwatershed.bounds\n\n(-96.57528919, 39.08361847, -96.55282197, 39.09784962)\n\n\nIn our next step we will try to learn how to generate the random coordinates and convert these coordinates into a Shapely MultiPoint object. We will re-write part of this code in a later section once we know how to do this. We also need to see whether creating the points this way would work. We will need to confirm this visually.\n\n# For reproducibility\nnp.random.seed(1)\n\n# Generate N random points\nN = 30\nrand_lon = np.random.uniform(low=watershed.bounds[0], high=watershed.bounds[2], size=N)\nrand_lat = np.random.uniform(low=watershed.bounds[1], high=watershed.bounds[3], size=N)\n\n\n# Create tuples with Lat and Lon for each point\nrand_points = []\nfor n in range(len(rand_lon)):\n rand_points.append(Point(rand_lon[n],rand_lat[n]))\n\n\n# Visualize random points\n\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\n\n# Iterate over each point in MultiPoint object P\nfor p in rand_points: \n \n # If point is within watershed, then make it green, oterhwise red.\n if p.within(watershed):\n plt.scatter(p.x, p.y, marker='x', color='g')\n else:\n plt.scatter(p.x, p.y, marker='x', color='r')\n\nplt.axis('equal')\nplt.show()\n\n\n\n\n\n# Use what we learned to create a list of points, all within the boundary\n\n# For reproducibility\nnp.random.seed(1)\n\n# Empty list to hold random points\nrand_points = []\nwhile len(rand_points) < N:\n \n # Generate random latitude and longitude\n rand_lon = np.random.uniform(low=watershed.bounds[0], high=watershed.bounds[2])\n rand_lat = np.random.uniform(low=watershed.bounds[1], high=watershed.bounds[3])\n \n # Convert the random lat and lon into a Shapely Point object\n point = Point(rand_lon, rand_lat)\n \n # Check if within watershed\n if point.within(watershed):\n rand_points.append(Point(rand_lon,rand_lat))\n\n\n# Visualize random points\n\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\n\nfor p in rand_points: \n if p.within(watershed):\n plt.scatter(p.x, p.y, marker='x', color='g')\n else:\n plt.scatter(p.x, p.y, marker='x', color='r')\nplt.axis('equal')\nplt.show()" + }, + { + "objectID": "exercises/field_random_samples.html#random-sampling-grid-cells", + "href": "exercises/field_random_samples.html#random-sampling-grid-cells", + "title": "80  Field random samples", + "section": "Random sampling grid cells", + "text": "Random sampling grid cells\nAnother option to random sampling points is to discretize the area of the water shed into N grid cells and then randomly select some of these cells to conduct the field sampling.\nTo solve this problem we will need to generate square grid cells and determined whether they are fully within the boundary of the watershed. This approach will make sure that no grid cell overlaps with the watershed boundary.\n\n# Create a test grid cell using the box method\n# box(minx, miny, maxx, maxy, ccw=True)\nb = box(-96.565, 39.090, -96.560, 39.095)\nb.exterior.xy\n\n(array('d', [-96.56, -96.56, -96.565, -96.565, -96.56]),\n array('d', [39.09, 39.095, 39.095, 39.09, 39.09]))\n\n\n\n# Extract coordinates from the box object\nb_lon, b_lat = list(b.exterior.coords.xy)\n\n\n# Test whehter box is COMPLETELY inside the watershed\n# For other options such as: intersect, overlaps, and touches see the docs\nb.within(watershed)\n\nTrue\n\n\n\n# Visualize that the arbitrary square is indeed within the watershed\n\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\nplt.plot(b_lon, b_lat, '-r')\nplt.axis('equal')\nplt.show()\n\n\n\n\n\nCreate grid\nTo create the grid cells within the watershed we have two options: 1) create a known number of cells that fit within the watershed, or 2) create as many grid cells as possible of a given size. Because of its irregular shape, we will create as many cells as possible of a given size. We will cover the entire bounding box of the water shed, and then eliminate those grdi cells that are not fully contained by the watershed boundaries.\nOf course, the smaller the size of the grid cells, the more cells we can fit, and the closer they will follow the perimeter of the watershed.\nAn important observation is that grid cells will share their sides, they will be touching each other, but they will not be overlapping.\n\n# Longitude vector\nx_vec = np.linspace(watershed.bounds[0], watershed.bounds[2], 30) \n\n# Latitude vector\ny_vec = np.linspace(watershed.bounds[1], watershed.bounds[3], 30)\n\nAn alternative that deserves some exploration is using the Numpy meshgrid() function and the Shapely MultiPolygon feature. In this particualr case I found that a straigth forward for loop and the use of an array of Shapely Polygons was simpler, at least for this particular problem.\n\n\nGenerate tuples for each grid cell\n\ngrid = []\nfor i in range(len(x_vec)-1):\n for j in range(len(y_vec)-1):\n cell = box(x_vec[i], y_vec[j], x_vec[i+1], y_vec[j+1])\n grid.append(cell)\n \n\n\n\nOverlay grid on watershed map\n\n# Visualize grid\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\n\nfor cell in grid:\n cell_lon = list(cell.exterior.coords.xy[0])\n cell_lat = list(cell.exterior.coords.xy[1])\n plt.plot(cell_lon,cell_lat, '-k', linewidth=0.5)\n \nplt.axis('equal')\nplt.show()\n\n\n\n\n\n\nExclude grid cells that are outside or overlap watershed\n\ngrid = []\nfor i in range(len(x_vec)-1):\n for j in range(len(y_vec)-1):\n cell = box(x_vec[i], y_vec[j], x_vec[i+1], y_vec[j+1]) \n if cell.within(watershed):\n grid.append(cell)\n\n\nplt.figure(figsize=(4,4))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\n\n\nfor cell in grid:\n cell_lon, cell_lat = list(cell.exterior.coords.xy)\n plt.plot(cell_lon, cell_lat, '-k', linewidth=0.5)\n \nplt.axis('equal')\nplt.show()\n\n\n\n\n\n\nSelect random numer of cell within watershed\n\n# For reproducibility\nnp.random.seed(99)\n\n# Select random cells from the set within the watershed\nsampling_cells = np.random.choice(grid, size=20, replace=False)\n\nplt.figure(figsize=(8,8))\nplt.title('Konza Prairie - Watershed K1B')\nplt.plot(lon_watershed,lat_watershed, '-k')\n\nfor cell in grid:\n cell_lon, cell_lat = list(cell.exterior.coords.xy)\n plt.plot(cell_lon,cell_lat, '-k', linewidth=0.5)\n \nfor count,cell in enumerate(sampling_cells):\n cell_lon, cell_lat = list(cell.exterior.coords.xy)\n plt.plot(cell_lon,cell_lat, '-r',linewidth=2)\n \n # Add count + 1 to start numbering from one. Add a little offset to improve visualization on map.\n plt.annotate(str(count+1), xy=(cell_lon[3]+0.0001, cell_lat[3]+0.0001))\n \nplt.axis('equal')\nplt.show()\n\n\n\n\n\n\nPrint centroid for each sampling cell\nThinking ahead on field work, it would be nice to have a the coordiantes for each cell. In this case we will print the Lower-Left corner of each cell. An alternative is to compute the cell centroid. You can easily do this using numpy. For instance:\n\nx_centroid = np.mean(cell_lon)\ny_centroid = np.mean(cell_lat)\n\nprint(\"Coordinates for the lower-left corner of the each cell\")\nfor count,cell in enumerate(sampling_cells):\n cell_lon, cell_lat = list(cell.exterior.coords.xy)\n print(\"Cell\",count+1,\"Lat:\", cell_lat[3],\"Lon:\",cell_lon[3])\n\nCoordinates for the lower-left corner of the each cell\nCell 1 Lat: 39.08509065793103 Lon: -96.55747036034482\nCell 2 Lat: 39.08656284586207 Lon: -96.5628934824138\nCell 3 Lat: 39.09196086827586 Lon: -96.55592089689655\nCell 4 Lat: 39.09196086827586 Lon: -96.55514616517242\nCell 5 Lat: 39.08999795103448 Lon: -96.56599240931035\nCell 6 Lat: 39.092451597586205 Lon: -96.55592089689655\nCell 7 Lat: 39.09147013896551 Lon: -96.55437143344827\nCell 8 Lat: 39.09196086827586 Lon: -96.5628934824138\nCell 9 Lat: 39.0880350337931 Lon: -96.55669562862069\nCell 10 Lat: 39.09294232689655 Lon: -96.56754187275862\nCell 11 Lat: 39.087544304482755 Lon: -96.55824509206896\nCell 12 Lat: 39.08999795103448 Lon: -96.56211875068965\nCell 13 Lat: 39.08656284586207 Lon: -96.55747036034482\nCell 14 Lat: 39.09147013896551 Lon: -96.56211875068965\nCell 15 Lat: 39.09196086827586 Lon: -96.55979455551724\nCell 16 Lat: 39.09294232689655 Lon: -96.56366821413793\nCell 17 Lat: 39.09294232689655 Lon: -96.56986606793104\nCell 18 Lat: 39.08705357517241 Lon: -96.56134401896551\nCell 19 Lat: 39.0934330562069 Lon: -96.57373972655174\nCell 20 Lat: 39.09392378551724 Lon: -96.56056928724138" + }, + { + "objectID": "exercises/yield_monitor_clean.html#dataset-description", + "href": "exercises/yield_monitor_clean.html#dataset-description", + "title": "81  Cleaning yield monitor data", + "section": "Dataset description", + "text": "Dataset description\nIn this exercise we will use a real yield monitor dataset from a soybean crop harvested in the state of Kansas in October of 2015. The farm and geogrpahic infromation were removed to preserve the farmers anonimity. So, the geographic coordinates were replaced by relative UTM coordinates. The X and Y coordinates are in meters relative to the center of the field (X=0, Y=0).\nUnits of variables in dataset:\n\nFlow in lbs/second\nArea in acres\nDistance in inches\nDuration in seconds\nYield in lbs/acre\nMoisture in percent\nWidth in inches" + }, + { + "objectID": "exercises/yield_monitor_clean.html#file-formats", + "href": "exercises/yield_monitor_clean.html#file-formats", + "title": "81  Cleaning yield monitor data", + "section": "File formats", + "text": "File formats\nYield monitor data is often saved using the Shapefile format (.shp). In this case I saved the file in .csv format to enable everyone to access the exercise using the pandas library.\nFor those interested in applying the same techniques to their own yield monitor data in .shp format, I recomend installing the geopandas library. The line sbelow should get you started.\n!pip install geopandas # Run in separate cell\ndf = gpd.read_file(\"../datasets/yield_monitor.shp\")\ndf.head(\n\n#import geopandas as gpd\nimport pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\n\ndf = pd.read_csv(\"../datasets/yield_monitor.csv\")\nprint(df.shape)\ndf.head()\n\n(3281, 12)\n\n\n\n\n\n\n\n\n\nGeometry\nX\nY\nCrop\nTimeStamp\nYield\nFlow\nMoisture\nDuration\nDistance\nWidth\nArea\n\n\n\n\n0\nPoint\n-156.576874\n-69.681188\nSoybeans\n2015-10-10 15:07:35\n50.51\n5.6990\n13.0\n1.0\n25.1969\n468.1102\n0.001880\n\n\n1\nPoint\n-156.100303\n-70.083748\nSoybeans\n2015-10-10 15:07:36\n51.41\n5.8003\n13.0\n1.0\n25.1969\n468.1102\n0.001880\n\n\n2\nPoint\n-155.632047\n-70.508652\nSoybeans\n2015-10-10 15:07:37\n50.66\n5.7151\n13.0\n1.0\n25.1969\n468.1102\n0.001880\n\n\n3\nPoint\n-155.198382\n-70.945248\nSoybeans\n2015-10-10 15:07:38\n54.20\n5.8286\n13.0\n1.0\n24.0157\n468.1102\n0.001792\n\n\n4\nPoint\n-154.808573\n-71.360394\nSoybeans\n2015-10-10 15:07:39\n55.86\n5.5141\n13.0\n1.0\n22.0472\n468.1102\n0.001645\n\n\n\n\n\n\n\n\n# Convert dates to Pandas datetime format\ndf[\"TimeStamp\"] = pd.to_datetime(df[\"TimeStamp\"], format=\"%Y/%m/%d %H:%M:%S\")\n\n\n# Compute speed in miles per hour (mph)\ndf[\"Speed\"] = df[\"Distance\"] / df[\"Duration\"] # in inches/second\ndf[\"Speed\"] = df[\"Speed\"]/63360*3600 # convert to miles/hour\n\n\n# Examine data\nplt.scatter(df[\"X\"], df[\"Y\"], s=10, c=df[\"Yield\"])\nplt.xlabel('Easting')\nplt.ylabel('Northing')\nplt.colorbar(label=\"Yield (lbs/acre)\")\nplt.show()\n\n\n\n\nAn extrmely useful habit is to plot the data. Histograms are great at describing the central tendency and dispersion of a given variable in a single figure. Based on histograms we can select and fine-tune the variables and thresholds that we will use to denoise our yield monitor data.\n\nplt.figure(figsize=(10,8))\nplt.subplot(2,2,1)\nplt.hist(df[\"Yield\"], bins=13)\nplt.xlabel(\"Yield (lbs/acre)\", size=16)\n\nplt.subplot(2,2,2)\nplt.hist(df[\"Flow\"])\nplt.xlabel(\"Flow (lbs/second)\", size=16)\n\nplt.subplot(2,2,3)\nplt.hist(df[\"Speed\"])\nplt.xlabel(\"Combine speed (mph)\", size=16)\n\nplt.subplot(2,2,4)\nplt.hist(df[\"Moisture\"])\nplt.xlabel(\"Grain moisture (%)\", size=16)\n\nplt.show()\n\n\n\n\n\n# Create copy of original DataFrame\ndf_clean = df" + }, + { + "objectID": "exercises/yield_monitor_clean.html#filtering-rules-with-clear-physical-meaning", + "href": "exercises/yield_monitor_clean.html#filtering-rules-with-clear-physical-meaning", + "title": "81  Cleaning yield monitor data", + "section": "Filtering rules with clear physical meaning", + "text": "Filtering rules with clear physical meaning\n\nidx_too_slow = df[\"Speed\"] < 2.5\nidx_too_fast = df[\"Speed\"] > 4\nidx_too_wet = df[\"Moisture\"] > 20\nidx_too_dry = df[\"Moisture\"] < 10\nidx_low_flow = df[\"Flow\"] <= 10\nidx_high_flow = df[\"Flow\"] >= 18\nidx = idx_too_slow | idx_too_fast | idx_too_dry | idx_too_wet | idx_low_flow | idx_high_flow\ndf_clean = df[~idx]\n\nUp to here, the method will probably clean most maps from yield monitors. If you want to stop here I also suggest adding the following two boolean conditions to filter out outliers. If you decide to implement a more sophisticaded approach, then you can probably omit the next two lines since we will be implementing a moving filter that will capture outliers later on.\nidx_high_yield = df[\"Yield\"] >= df[\"Yield\"].quantile(0.95)\nidx_low_yield = df[\"Yield\"] <= df[\"Yield\"].quantile(0.05)\n\nprint(df.shape)\nprint(df_clean.shape)\nremoved_points = df.shape[0] - df_clean.shape[0]\nprint(\"Removed\",str(removed_points),'points')\n\n(3281, 13)\n(2760, 13)\nRemoved 521 points\n\n\nLet’s generate a plot showing the original and resulting dataset after our first layer of cleaning. We will circle the points that we removed to check our work.\n\nplt.figure(figsize=(12,12))\n\nplt.subplot(2,1,1)\nplt.scatter(df[\"X\"], df[\"Y\"], s=40, marker='s', c=df[\"Yield\"])\nplt.colorbar(label=\"Yield (lbs/acre)\")\nplt.clim(0,70)\nplt.scatter(df.loc[idx,\"X\"], df.loc[idx,\"Y\"], \n s=80,\n marker='o', \n facecolor=None, \n edgecolor='r', \n alpha=0.8)\nplt.xlabel('Easting')\nplt.ylabel('Northing')\n\nplt.subplot(2,1,2)\nplt.scatter(df_clean[\"X\"], df_clean[\"Y\"], s=40, marker='s', c=df_clean[\"Yield\"])\nplt.colorbar(label=\"Yield (lbs/acre)\")\nplt.clim(0,70)\nplt.xlabel('Easting')\nplt.ylabel('Northing')\n\nplt.show()\n\n\n\n\n\n# Remove NaNs\ndf_clean = df_clean.dropna()\n\n\nplt.figure(figsize=(8,6))\nplt.tricontourf(df_clean[\"X\"], df_clean[\"Y\"], df_clean[\"Yield\"], levels=7)\nplt.axis('equal')\nplt.show()\n\n\n\n\n\n# Reset index\ndf_clean.reset_index(drop=True, inplace=True)\ndf_clean.head()\n\n\n\n\n\n\n\n\nGeometry\nX\nY\nCrop\nTimeStamp\nYield\nFlow\nMoisture\nDuration\nDistance\nWidth\nArea\nSpeed\n\n\n\n\n0\nPoint\n-116.700755\n-75.081549\nSoybeans\n2015-10-10 15:08:21\n59.83\n12.1293\n10.2\n1.0\n45.2756\n468.1102\n0.003379\n2.572477\n\n\n1\nPoint\n-115.518013\n-75.072397\nSoybeans\n2015-10-10 15:08:22\n49.25\n10.1585\n10.4\n1.0\n46.0630\n468.1102\n0.003438\n2.617216\n\n\n2\nPoint\n-114.387442\n-75.064138\nSoybeans\n2015-10-10 15:08:23\n56.08\n11.0729\n10.9\n1.0\n44.0945\n468.1102\n0.003291\n2.505369\n\n\n3\nPoint\n-113.230785\n-75.055433\nSoybeans\n2015-10-10 15:08:24\n51.03\n10.2547\n11.2\n1.0\n44.8819\n468.1102\n0.003349\n2.550108\n\n\n4\nPoint\n-112.073939\n-75.057824\nSoybeans\n2015-10-10 15:08:25\n51.44\n10.4287\n11.5\n1.0\n45.2756\n468.1102\n0.003379\n2.572477" + }, + { + "objectID": "exercises/yield_monitor_clean.html#moving-filters", + "href": "exercises/yield_monitor_clean.html#moving-filters", + "title": "81  Cleaning yield monitor data", + "section": "Moving filters", + "text": "Moving filters\nA moving filter is usually a sliding window that performs a smoothing operation. This usually works great with regular grids, but in the case of yield monitor data we deal with irregular grids, which requires that we handle the window in a slighly different way.\nWe will iterate over each point collected by the combine and at each iteration step we will find the observations within a given distance from the combine and we will either:\n\nAssign the current point the median value of all its neighboring points within a specific distance radius.\nUse the yield value of all the selected neighboring points to determine whether the value of the current point is within the 5 and 95th percentile of the local yields.\n\nCertainly there are many other options. I just came up with these two simple approaches based on previous studies and my own experience.\nIn any case, we need to be able to compute the distance from the current point in the iteration to all the other points. This is how we will determine the neighbors.\n\nFunction to compute Euclidean distance\n\ndef edist(xpoint,ypoint,xvec,yvec):\n \"\"\"Compute and sort Euclidean distance from a point to all other points.\"\"\"\n distance = np.sqrt((xpoint - xvec)**2 + (ypoint - yvec)**2)\n idx = np.argsort(distance)\n \n return {\"distance\":distance, \"idx\":idx}\n\n\n\nMoving median filter\n\ndf_medfilter = df_clean\n\nfor i in range(df_medfilter.shape[0]):\n \n # Compute Euclidean distance from each point to the rest of the points\n D = edist(df_medfilter[\"X\"][i], df_medfilter[\"Y\"][i], df_medfilter[\"X\"], df_medfilter[\"Y\"])\n\n \n # Find index of neighbors\n idx = D[\"distance\"] <= 5 # meters\n \n # Replace current value with median of neighbors\n df_medfilter.loc[i,\"Yield\"] = df_medfilter.loc[idx,\"Yield\"].median()\n\n# This can take a while, so let's print something to know that the interpreter is done.\nprint('Done!')\n\nDone!\n\n\n\nplt.figure(figsize=(12,4))\n\nplt.subplot(1,2,1)\nplt.scatter(df_medfilter[\"X\"], df_medfilter[\"Y\"], s=10, c=df_medfilter[\"Yield\"])\nplt.colorbar()\nplt.clim(20, 70)\nplt.axis('equal')\n\nplt.subplot(1,2,2)\nplt.tricontourf(df_medfilter[\"X\"], df_medfilter[\"Y\"], df_medfilter[\"Yield\"])\nplt.axis('equal')\n\nplt.show()\n\n\n\n\n\n\nMoving filter to detect outliers\n\ndf_outfilter = df_clean.copy()\n\nfor i in range(df_outfilter.shape[0]):\n \n # Compute Euclidean distance from each point to the rest of the points\n D = edist(df_outfilter[\"X\"][i], df_outfilter[\"Y\"][i], df_outfilter[\"X\"], df_outfilter[\"Y\"])\n \n idx = D[\"distance\"] <= 5 # meters\n current_point_yield = df_outfilter.loc[i,\"Yield\"]\n \n # Find lower and upper threshold based on percentiles\n Q5 = df_outfilter.loc[idx,\"Yield\"].quantile(0.05)\n Q95 = df_outfilter.loc[idx,\"Yield\"].quantile(0.95)\n \n # If current point is lower or greater than plausible neighbor values, then set to NaN\n if (current_point_yield < Q5) | (current_point_yield > Q95):\n df_outfilter.loc[i,\"Yield\"] = np.nan\n\nprint(\"Done!\")\n\nDone!\n\n\nCheck if your method detected any outliers, which should have been assigned NaN to the yield variable.\n\ndf_outfilter[\"Yield\"].isna().sum()\n\n392\n\n\nThere are some NaN values. The interpolation method tricontourf does not handle NaN, so we need to remove them from the DataFrame first. We will use the Pandas dropna() to do this easily.\n\ndf_outfilter = df_outfilter.dropna()\ndf_outfilter[\"Yield\"].isna().sum()\n\n0\n\n\nPerfect, our DataFrame no longer contains NaN values. Before we proceed, let’s also check the difference in the number of points between the two methods. Just to know whether our filtering was a bit execessive. If it was, then realexed some of the initial paramters or the quantiles threshlds.\n\nprint(df_medfilter.shape)\nprint(df_outfilter.shape)\n\n(2760, 13)\n(2368, 13)\n\n\n\nplt.figure(figsize=(12,4))\n\nplt.subplot(1,2,1)\nplt.scatter(df_outfilter[\"X\"], df_outfilter[\"Y\"], s=10, c=df_outfilter[\"Yield\"])\nplt.colorbar()\nplt.clim(20, 70)\nplt.axis('equal')\n\nplt.subplot(1,2,2)\nplt.tricontourf(df_outfilter[\"X\"], df_outfilter[\"Y\"], df_outfilter[\"Yield\"])\nplt.axis('equal')\n\nplt.show()" + }, + { + "objectID": "exercises/yield_monitor_clean.html#observations", + "href": "exercises/yield_monitor_clean.html#observations", + "title": "81  Cleaning yield monitor data", + "section": "Observations", + "text": "Observations\nThe approaches tested in exercise yieldded somewhat similar results. The first layer of outliers removal based on the plausible value of variables with clear physical meaning such as combine spped, grain flow rate, and grain moisture content are an effective way of removing clear outliers.\nThe first layer does note result in a clear interpolation, at least using the tricontourf function. I’m sure that some of the filters with scipy and image processing toolboxes can solve this issue without further processing.\nA median filter is a powerful filter widely used in image analysis and was effective to remove outliers and dramatically improved the map in terms of smoothness and visual patterns. This method does not preserve the original values.\nThe moving window to remove outliers based on quantile thresholds performed similar to the median filter and represents an alternative method. This method preserves the original values and removes values that are considered outliers.\nThe method of choice depends on the user, the complexity of the data, and the performance of the methods compared to known field patterns and observations during harvest." + }, + { + "objectID": "exercises/yield_monitor_clean.html#references", + "href": "exercises/yield_monitor_clean.html#references", + "title": "81  Cleaning yield monitor data", + "section": "References", + "text": "References\nKhosla, R. and Flynn, B., 2008. Understanding and cleaning yield monitor data. Soil Science Step-by-Step Field Analysis, (soilsciencestep), pp.113-130.\nKleinjan, J., Chang, J., Wilson, J., Humburg, D., Carlson, G., Clay, D. and Long, D., 2002. Cleaning yield data. SDSU Publication." + }, + { + "objectID": "exercises/yield_monitor_zones.html", + "href": "exercises/yield_monitor_zones.html", + "title": "82  Management zones yield monitor", + "section": "", + "text": "Yield monitor data collected from sensors onboard of modern combines offer a wealth of spatial information crucial for precision agriculture. By analyzing these data, farmers and agronomists can create and delineate field management zones based on varying crop yield levels. This process involves importing, cleaning, and aggregating yield observations across a field to identify areas that exhibit similar yield levels.\nThese management zones can then be used to apply specific agronomic practices, such as variable rate seeding, fertilization, or irrigation, which are adjusted to the specific needs of each zone. By segmenting fields based on yield data, this approach allows for more efficient use of resources, optimizing crop production, and more sustainable farming practices by reducing unnecessary inputs in lower-yielding areas.\nIn this exercise we will use a yield monitor data to implement a series of geospatial operations to remove outliers, clip the observations to the field boundaries, and cluster the resulting yield map.\nGo to https://geojson.io/ and export a field boundary for the field. Just in case, the boundary is also available in the “datasets/shapefiles/Mortimers” folder\n\n# Import modules\nimport numpy as np\nimport geopandas as gpd\nimport matplotlib.pyplot as plt\nfrom shapely.geometry import box\nfrom scipy.interpolate import griddata\n\n\n# Read the file\ngdf = gpd.read_file(\"../datasets/spatial/Mortimers/Soybeans.shp\")\ngdf.head(3)\n\n\n\n\n\n\n\n\nGrowerName\nFieldName\nCrpZnName\nLoad\nVariety\nLoadVar\nMachineID\nTimeStamp\nDate\nTime\nYield\nFlow\nMoisture\nDuration\nDistance\nWidth\nArea\ngeometry\n\n\n\n\n0\nKnopf Farms\nMortimer's\nSoybeans\n17/09/30-18:37:10\nNaN\n17/09/30-18:37:10\n222486\n2017-09-30 18:38:56\n2017-09-30\n18:38:56\n61.45\n2.7965\n13.0\n1.0\n21.8504\n480.0\n0.001672\nPOINT (-97.43268 38.71256)\n\n\n1\nKnopf Farms\nMortimer's\nSoybeans\n17/09/30-18:37:10\nNaN\n17/09/30-18:37:10\n222486\n2017-09-30 18:38:57\n2017-09-30\n18:38:57\n49.74\n2.1531\n13.0\n1.0\n20.7874\n480.0\n0.001591\nPOINT (-97.43269 38.71255)\n\n\n2\nKnopf Farms\nMortimer's\nSoybeans\n17/09/30-18:37:10\nNaN\n17/09/30-18:37:10\n222486\n2017-09-30 18:38:58\n2017-09-30\n18:38:58\n42.12\n1.8202\n13.0\n1.0\n20.7480\n480.0\n0.001588\nPOINT (-97.43269 38.71255)\n\n\n\n\n\n\n\n\n# Add a speed column\ngdf['Speed'] = gdf['Distance']/gdf['Duration']/12*0.68 # in/s to mph\n\n\n# Inspect data in tabular format\ngdf[['Yield','Moisture','Speed']].describe()\n\n\n\n\n\n\n\n\nYield\nMoisture\nSpeed\n\n\n\n\ncount\n36426.000000\n36426.000000\n36426.000000\n\n\nmean\n46.005093\n10.472791\n3.613725\n\n\nstd\n38.166806\n2.189875\n0.785171\n\n\nmin\n0.000000\n4.700000\n0.267716\n\n\n25%\n34.080000\n9.600000\n3.043045\n\n\n50%\n44.690000\n9.900000\n3.725720\n\n\n75%\n56.467500\n10.100000\n4.232150\n\n\nmax\n4150.990000\n18.100000\n6.893698\n\n\n\n\n\n\n\n\n# Visualize data\nfig, ax = plt.subplots(nrows=1, ncols=1)\ngdf.plot(ax=ax,\n column='Yield',\n k=8,\n scheme='quantiles',\n cmap='RdYlGn',\n legend=True,\n marker='.',\n markersize=10,\n legend_kwds={'loc':'upper left', 'bbox_to_anchor':(1.05,1), 'title':'Grain yield (lbs/harvested area)'});\n\nax.ticklabel_format(useOffset=False)\n\n\n\n\n\n# Read boundaries of the file \nbnd = gpd.read_file('../datasets/spatial/Mortimers/mortimer_bnd.geojson')\nbnd.head()\n\n\n\n\n\n\n\n\ngeometry\n\n\n\n\n0\nPOLYGON ((-97.43696 38.71800, -97.43692 38.714...\n\n\n\n\n\n\n\n\n# Clip the yield monitor data to field boundaries\ngdf = gpd.clip(gdf, bnd['geometry'].iloc[0])\ngdf.reset_index(drop=True, inplace=True)\ngdf.head()\n\n\n\n\n\n\n\n\nGrowerName\nFieldName\nCrpZnName\nLoad\nVariety\nLoadVar\nMachineID\nTimeStamp\nDate\nTime\nYield\nFlow\nMoisture\nDuration\nDistance\nWidth\nArea\ngeometry\nSpeed\n\n\n\n\n0\nKnopf Farms\nMortimer's\nSoybeans\n11\nNaN\n11\n222486\n2017-10-02 11:44:21\n2017-10-02\n11:44:21\n5.21\n0.7464\n13.0\n1.0\n68.8189\n480.0\n0.005266\nPOINT (-97.43575 38.71092)\n3.899738\n\n\n1\nKnopf Farms\nMortimer's\nSoybeans\n11\nNaN\n11\n222486\n2017-10-02 11:44:20\n2017-10-02\n11:44:20\n6.94\n0.9996\n13.0\n1.0\n69.1732\n480.0\n0.005293\nPOINT (-97.43573 38.71092)\n3.919815\n\n\n2\nKnopf Farms\nMortimer's\nSoybeans\n11\nNaN\n11\n222486\n2017-10-02 11:44:23\n2017-10-02\n11:44:23\n6.07\n0.8866\n13.0\n1.0\n70.0787\n480.0\n0.005363\nPOINT (-97.43579 38.71092)\n3.971126\n\n\n3\nKnopf Farms\nMortimer's\nSoybeans\n11\nNaN\n11\n222486\n2017-10-02 11:44:22\n2017-10-02\n11:44:22\n4.22\n0.6124\n13.0\n1.0\n69.7244\n480.0\n0.005336\nPOINT (-97.43577 38.71092)\n3.951049\n\n\n4\nKnopf Farms\nMortimer's\nSoybeans\n11\nNaN\n11\n222486\n2017-10-02 11:44:19\n2017-10-02\n11:44:19\n7.09\n1.0478\n13.0\n1.0\n70.9449\n480.0\n0.005429\nPOINT (-97.43571 38.71092)\n4.020211\n\n\n\n\n\n\n\n\n# Visualize data\nfig, ax = plt.subplots(nrows=1, ncols=1)\ngdf.plot(ax=ax,\n column='Yield',\n k=8,\n scheme='quantiles',\n cmap='RdYlGn',\n legend=True,\n marker='.',\n markersize=10,\n legend_kwds={'loc':'upper left', 'bbox_to_anchor':(1.05,1), 'title':'Grain yield (lbs/harvested area)'});\n\nbnd.plot(ax=ax, linewidth=2, facecolor='None', edgecolor='k')\n\nax.ticklabel_format(useOffset=False)\n\n\n\n\n\n\n# Create histograms of yield and speed\nplt.figure(figsize=(8,3))\n\nplt.subplot(1,2,1)\nplt.hist(gdf['Yield'], bins='scott')\nplt.xlabel('Yield (lbs/harvested acre)')\n\nplt.subplot(1,2,2)\nplt.hist(gdf['Speed'], bins='scott', edgecolor='k', color='skyblue')\nplt.xlabel('Speed (mph)')\n\nplt.show()\n\n\n\n\n\n# Create a function to apply the IQR method for detecting outliers\n\ndef iqr_fn(y):\n '''Function to compute the IQR'''\n quantiles = [0.25, 0.75]\n q1,q3 = y.quantile(quantiles)\n iqr = q3 - q1\n idx_outliers = (y < q1-1.5*iqr) | (y > q3+1.5*iqr)\n return idx_outliers\n\n\n# Use our function to find outliers\nidx_yield = iqr_fn(gdf['Yield'])\nidx_speed = iqr_fn(gdf['Speed'])\n\nidx_all_outliers = idx_yield | idx_speed\ngdf.loc[idx_all_outliers, 'Yield'] = np.nan\n\n\n# Visualize data\nfig, ax = plt.subplots(nrows=1, ncols=1)\ngdf.plot(ax=ax,\n column='Yield',\n k=8,\n scheme='quantiles',\n cmap='RdYlGn',\n legend=True,\n marker='.',\n markersize=10,\n legend_kwds={'loc':'upper left', 'bbox_to_anchor':(1.05,1), 'title':'Grain yield (lbs/harvested area)'});\n\nbnd.plot(ax=ax, linewidth=2, facecolor='None', edgecolor='k')\n\nax.ticklabel_format(useOffset=False)\n\n\n\n\n\n# Learn what the box function does\nbox(0,0, 10, 10)\n\n\n\n\n\n# Specify coordinate reference systems\nepsg_utm = 32614 # UTM Zone 14\nepsg_wgs = 4326 # WGS84\n\n\n# Define boxes of the field\nxmin, ymin, xmax, ymax = bnd.to_crs(epsg=epsg_utm).total_bounds\n\n# Define box size\nxdelta = 20 # meters\nydelta = 20 # meters\n\n# Create an empty numpy array\ngrid = np.array([])\n\n# Iterate to create each box\nfor x in np.arange(xmin,xmax,xdelta):\n for y in np.arange(ymin,ymax,ydelta):\n cell = box(x, y, x+xdelta, y+ydelta)\n grid = np.append(grid, cell)\n \n# Conver the grid to a GeoDataFrame\ngdf_grid = gpd.GeoDataFrame(grid, columns=['geometry'], crs=epsg_utm)\n\n# Get centroids of each cell\ngdf_grid['centroids'] = gdf_grid['geometry'].centroid\n\ngdf_grid.head()\n\n\n\n\n\n\n\n\ngeometry\ncentroids\n\n\n\n\n0\nPOLYGON ((635905.410 4285847.629, 635905.410 4...\nPOINT (635895.410 4285857.629)\n\n\n1\nPOLYGON ((635905.410 4285867.629, 635905.410 4...\nPOINT (635895.410 4285877.629)\n\n\n2\nPOLYGON ((635905.410 4285887.629, 635905.410 4...\nPOINT (635895.410 4285897.629)\n\n\n3\nPOLYGON ((635905.410 4285907.629, 635905.410 4...\nPOINT (635895.410 4285917.629)\n\n\n4\nPOLYGON ((635905.410 4285927.629, 635905.410 4...\nPOINT (635895.410 4285937.629)\n\n\n\n\n\n\n\n\n# Change CRS\ngdf_grid['geometry'] = gdf_grid['geometry'].to_crs(epsg=epsg_wgs)\ngdf_grid['centroids'] = gdf_grid['centroids'].to_crs(epsg=epsg_wgs)\n\n\n# Create plot of field boundary and grid\nfig, ax = plt.subplots(1,1)\nbnd.plot(ax=ax, facecolor='w', edgecolor='r')\ngdf_grid.plot(ax=ax, facecolor='None', edgecolor='k')\nax.ticklabel_format(useOffset=False)\nplt.show()\n\n\n\n\n\n# Clip cells to field boundary\ngdf_grid = gpd.clip(gdf_grid, bnd['geometry'].iloc[0])\ngdf_grid.reset_index(drop=True, inplace=True)\n\n\n# Create plot of field boundary and grid\nfig, ax = plt.subplots(1,1)\nbnd.plot(ax=ax, facecolor='w', edgecolor='r')\ngdf_grid.plot(ax=ax, facecolor='None', edgecolor='k')\nax.ticklabel_format(useOffset=False)\nplt.show()\n\n\n\n\n\n# Iterate over each cell to compute the median yield\ngrid_yield = []\nfor k,row in gdf_grid.iterrows():\n idx_within_cell = gdf['geometry'].within(row['geometry'])\n yield_value = gdf.loc[idx_within_cell, 'Yield'].median()\n grid_yield.append(yield_value)\n \n# Append new column to the gdf_grid \ngdf_grid['yield'] = grid_yield\ngdf_grid.head()\n\n\n\n\n\n\n\n\ngeometry\ncentroids\nyield\n\n\n\n\n0\nPOLYGON ((-97.43229 38.71095, -97.43229 38.710...\nPOINT (-97.43217 38.71086)\nNaN\n\n\n1\nPOLYGON ((-97.43252 38.71095, -97.43229 38.710...\nPOINT (-97.43240 38.71086)\nNaN\n\n\n2\nPOLYGON ((-97.43229 38.71095, -97.43229 38.710...\nPOINT (-97.43217 38.71104)\nNaN\n\n\n3\nPOLYGON ((-97.43229 38.71095, -97.43252 38.710...\nPOINT (-97.43240 38.71104)\n36.565\n\n\n4\nPOLYGON ((-97.43251 38.71113, -97.43251 38.711...\nPOINT (-97.43240 38.71122)\n53.040\n\n\n\n\n\n\n\n\n# Create plot of field boundary and grid\nfig, ax = plt.subplots(1,1)\nbnd.plot(ax=ax, facecolor='w', edgecolor='k')\ngdf_grid.plot(ax=ax, column='yield', edgecolor='k', cmap='RdYlGn')\nax.ticklabel_format(useOffset=False)\nplt.show()\n\n\n\n\n\n# CLustering using K-Means\nfrom sklearn.cluster import KMeans\n\ngdf_grid.dropna(inplace=True)\nkmeans_info = KMeans(n_clusters=3, n_init='auto').fit(gdf_grid['yield'].values.reshape(-1,1))\ngdf_grid['zone'] = kmeans_info.labels_.flatten()\ngdf_grid.head()\n\n\n\n\n\n\n\n\ngeometry\ncentroids\nyield\nzone\n\n\n\n\n3\nPOLYGON ((-97.43229 38.71095, -97.43252 38.710...\nPOINT (-97.43240 38.71104)\n36.565\n2\n\n\n4\nPOLYGON ((-97.43251 38.71113, -97.43251 38.711...\nPOINT (-97.43240 38.71122)\n53.040\n0\n\n\n5\nPOLYGON ((-97.43251 38.71131, -97.43250 38.711...\nPOINT (-97.43239 38.71140)\n46.680\n2\n\n\n7\nPOLYGON ((-97.43275 38.71096, -97.43252 38.710...\nPOINT (-97.43263 38.71087)\n47.360\n2\n\n\n8\nPOLYGON ((-97.43298 38.71096, -97.43275 38.710...\nPOINT (-97.43286 38.71087)\n75.450\n1\n\n\n\n\n\n\n\n\n# Visualize the management zones\n# Create plot of field boundary and grid\nfig, ax = plt.subplots(1,3, figsize=(14,12))\n\ngdf.plot(ax=ax[0],\n column='Yield',\n k=8,\n scheme='quantiles',\n cmap='RdYlGn',\n legend=True,\n marker='.',\n markersize=10,\n legend_kwds={'title':'Grain yield (lbs/harvested area)'});\nbnd.plot(ax=ax[0], linewidth=2, facecolor='None', edgecolor='k')\n\nbnd.plot(ax=ax[1], facecolor='w', edgecolor='k')\ngdf_grid.plot(ax=ax[1], column='yield', edgecolor='k', cmap='RdYlGn')\n\nbnd.plot(ax=ax[2], facecolor='w', edgecolor='k')\ngdf_grid.plot(ax=ax[2], column='zone', edgecolor='k', cmap='Set3')\n\nax[0].ticklabel_format(useOffset=False)\nax[1].ticklabel_format(useOffset=False)\nax[2].ticklabel_format(useOffset=False)\n\nplt.show()\n\n\n\n\n\n# Create interactive map\nm = gdf.explore(column='Yield', cmap='RdYlGn', tooltip='Yield',popup=True,\n tiles=\"CartoDB positron\",\n style_kwds=dict(color=\"None\", fillOpacity=1.0), # use black outline\n tooltip_kwds=dict(aliases=['Yield (%)'])\n )\nm\n\n\nMake this Notebook Trusted to load map: File -> Trust Notebook" + }, + { + "objectID": "exercises/largest_empty_circle.html#define-coordinate-reference-systems", + "href": "exercises/largest_empty_circle.html#define-coordinate-reference-systems", + "title": "83  Largest empty circle", + "section": "Define coordinate reference systems", + "text": "Define coordinate reference systems\nWhen computing distances on a map, it is often easier to work in projected coordinates (e.g., Universal Transverse Mercator). So, during our exercise we will be converting between geographic and projected coordinates as needed using the UTM-14 and WGS84 coordinate reference systems. The UTM-14 zone fits well for Kansas, but won’t be good for other regions. Here is a map for the contiguous U.S. You can learn more about the UTM coordiante system here\n\nBy Chrismurf at English Wikipedia, CC BY 3.0, Link\n\n\n# Define projected and geographic reference systems\nepsg_utm = CRS.from_dict({'proj':'utm', 'zone':14, 'south':False}).to_epsg()\nepsg_wgs = 4326 # WGS84" + }, + { + "objectID": "exercises/largest_empty_circle.html#load-stations-dataset", + "href": "exercises/largest_empty_circle.html#load-stations-dataset", + "title": "83  Largest empty circle", + "section": "Load stations dataset", + "text": "Load stations dataset\n\n# Read stations\nstations = gpd.read_file('../datasets/KS_mesonet_geoinfo.csv')\nstations[['lon','lat']] = stations[['lon','lat']].astype('float')\nstations['geometry'] = gpd.points_from_xy(x=stations['lon'], y=stations['lat'], crs=4326).to_crs(epsg_wgs)" + }, + { + "objectID": "exercises/largest_empty_circle.html#load-maps", + "href": "exercises/largest_empty_circle.html#load-maps", + "title": "83  Largest empty circle", + "section": "Load maps", + "text": "Load maps\n\n# Read state boundary map (already in WGS84)\nstates = gpd.read_file('../datasets/spatial/us_state_5m.geojson') #.to_crs(epsg_wgs)\nidx_state = states['NAME'] == 'Kansas'\nbnd = states.loc[idx_state].reset_index(drop=True)\n\n# Simplify state boundary for faster computation in later processing\nbnd = bnd.to_crs(epsg_utm).simplify(100).to_crs(epsg_wgs)\n\n# Read counties map (only for visuals, not used in any core computation) (already in WGS84)\ncounties = gpd.read_file('../datasets/spatial/us_county_5m.geojson') #.to_crs(epsg_wgs)\nidx_state = counties['STATE_NAME'] == 'Kansas'\ncounties = counties.loc[idx_state].reset_index(drop=True)" + }, + { + "objectID": "exercises/largest_empty_circle.html#visualize-map-and-stations", + "href": "exercises/largest_empty_circle.html#visualize-map-and-stations", + "title": "83  Largest empty circle", + "section": "Visualize map and stations", + "text": "Visualize map and stations\n\n# Creaet figure using object-based syntax\nfig, ax = plt.subplots(1, 1, figsize=(6,4))\ncounties.plot(ax=ax, facecolor='None', edgecolor='gray', linewidth=0.25)\nbnd.plot(ax=ax, facecolor='None', edgecolor='k')\nstations.plot(ax=ax, facecolor='tomato', edgecolor='k')\n#ax.ticklabel_format(useOffset=False) # USe this line when working in UTM\nplt.show()" + }, + { + "objectID": "exercises/largest_empty_circle.html#compute-voronoi-polygons", + "href": "exercises/largest_empty_circle.html#compute-voronoi-polygons", + "title": "83  Largest empty circle", + "section": "Compute voronoi polygons", + "text": "Compute voronoi polygons\n\n# Merge coordinates in different columns into a list of tuples\ncoords = list(zip(stations['geometry'].x, stations['geometry'].y))\n\n# Compute voronoi polygons. Note: Centroids are the same as the stations\nvoronoi_poly, voronoi_centroids = voronoi_frames(coords, clip=bnd.iloc[0])\n\n# Add CRS to resulting voronoi polygons\nvoronoi_poly.set_crs(epsg=epsg_wgs, inplace=True);\n\n# Compute area for each voronoi polygon\nvoronoi_poly['area'] = voronoi_poly.to_crs(epsg=epsg_utm).area\n\n# Sort by largest area first\nvoronoi_poly.sort_values(by='area', inplace=True, ascending=False)\nvoronoi_poly.reset_index(drop=True, inplace=True)\n\n# Visualize voronoi polygons and points\nfig, ax = plt.subplots(1, 1, figsize=(6,4))\nvoronoi_poly.plot(ax=ax, facecolor='None', edgecolor='k')\nvoronoi_centroids.plot(ax=ax, facecolor='tomato', edgecolor='k')\nplt.show()" + }, + { + "objectID": "exercises/largest_empty_circle.html#get-vertices-of-voronoi-polygons", + "href": "exercises/largest_empty_circle.html#get-vertices-of-voronoi-polygons", + "title": "83  Largest empty circle", + "section": "Get vertices of voronoi polygons", + "text": "Get vertices of voronoi polygons\n\n# Gather vertices to use as tentative centroids to find the LEC\ninclude_bnd_points = True\n\nif include_bnd_points:\n vertices = []\n for k,row in voronoi_poly.iterrows():\n vertices.extend(list(row['geometry'].exterior.coords))\n \n vertices = pd.DataFrame(vertices, columns=['lon','lat']).drop_duplicates().reset_index(drop=True)\n vertices['geometry'] = list(zip(vertices['lon'], vertices['lat']))\n vertices['geometry'] = vertices['geometry'].apply(Point)\n vertices = gpd.GeoDataFrame(vertices).set_crs(epsg=epsg_wgs)\n \nelse:\n polygons, vertices = voronoi(coords)\n vertices = pd.DataFrame(vertices, columns=['lon','lat']).drop_duplicates().reset_index(drop=True)\n vertices['geometry'] = list(zip(vertices['lon'], vertices['lat']))\n vertices['geometry'] = vertices['geometry'].apply(Point)\n vertices = gpd.GeoDataFrame(vertices, crs=epsg_wgs).clip(bnd.loc[0,'geometry'])" + }, + { + "objectID": "exercises/largest_empty_circle.html#find-remotest-point", + "href": "exercises/largest_empty_circle.html#find-remotest-point", + "title": "83  Largest empty circle", + "section": "Find remotest point", + "text": "Find remotest point\n\n# Compute the area of all clipped empty circles and find circle with largest area\ndf_lec = gpd.GeoDataFrame()\n\nempty_circles = []\n\n# Before computing distances, convert both dataframes to UTM coordinates\nstations.to_crs(epsg=epsg_utm, inplace=True)\nvertices.to_crs(epsg=epsg_utm, inplace=True)\n\nfor k,row in vertices.iterrows():\n \n gpd_row = gpd.GeoSeries(row['geometry'], crs=epsg_utm)\n radius = stations.distance(gpd_row.iloc[0]).sort_values().iloc[0] # Shortest radius in m\n circle_coords = gpd_row.buffer(radius).to_crs(epsg=epsg_wgs).clip(bnd.iloc[0])\n circle_area = circle_coords.to_crs(epsg=epsg_utm).area.values[0]\n \n # Save variables\n empty_circles.append({'geometry':gpd_row.values[0],\n 'circle_coords': circle_coords[0],\n 'circle_area': circle_area,\n 'radius': radius})\n\n# Convert dictionary to GeoDataframe (geometry is still in UTM)\ndf_empty_circles = gpd.GeoDataFrame(empty_circles, geometry='geometry', crs=epsg_utm)\n\n# Sort empty circles by decreasing area\ndf_empty_circles.sort_values(by='circle_area', ascending=False, inplace=True)\ndf_empty_circles.reset_index(drop=True, inplace=True)\n\n# Keep only largest empty circle\n#df_lec = df_lec.loc[[0]]\n \n# Restore geographic coordinates of geometries\nstations.to_crs(epsg=epsg_wgs, inplace=True)\nvertices.to_crs(epsg=epsg_wgs, inplace=True)\ndf_empty_circles.to_crs(epsg=epsg_wgs, inplace=True)\n\n# Show largest empty circle information\nlec = gpd.GeoDataFrame(df_empty_circles.iloc[[0]], geometry='geometry', crs=epsg_wgs)\nlec\n\n\n\n\n\n\n\n\ngeometry\ncircle_coords\ncircle_area\nradius\n\n\n\n\n0\nPOINT (-96.35989 38.34558)\nPOLYGON ((-95.52356334992069 38.25856568398934...\n1.709451e+10\n73824.810245" + }, + { + "objectID": "exercises/largest_empty_circle.html#append-tentative-location-of-new-station", + "href": "exercises/largest_empty_circle.html#append-tentative-location-of-new-station", + "title": "83  Largest empty circle", + "section": "Append tentative location of new station", + "text": "Append tentative location of new station\n\n# Append tentative centroid to the stations table\nnew_name = f\"station_{stations.shape[0]+1}\"\nnew_station = gpd.GeoDataFrame({'name':new_name,\n 'lat': [lec.loc[0,'geometry'].y],\n 'lon': [lec.loc[0,'geometry'].x],\n 'geometry': [lec.loc[0,'geometry']]},\n crs=epsg_wgs)\n\nupdated_stations = pd.concat([stations, new_station]).reset_index(drop=True)\nupdated_stations.tail()\n\n\n\n\n\n\n\n\nname\nlat\nlon\ngeometry\n\n\n\n\n52\nViola\n37.459700\n-97.62470\nPOINT (-97.62470 37.45970)\n\n\n53\nWallace\n38.819800\n-101.85300\nPOINT (-101.85300 38.81980)\n\n\n54\nWashington\n39.781200\n-97.05980\nPOINT (-97.05980 39.78120)\n\n\n55\nWoodson\n37.861200\n-95.78360\nPOINT (-95.78360 37.86120)\n\n\n56\nstation_57\n38.345583\n-96.35989\nPOINT (-96.35989 38.34558)" + }, + { + "objectID": "exercises/largest_empty_circle.html#show-the-larget-empty-circle", + "href": "exercises/largest_empty_circle.html#show-the-larget-empty-circle", + "title": "83  Largest empty circle", + "section": "Show the larget empty circle", + "text": "Show the larget empty circle\n\n# Create figure with resulting largest empty circle\n\n# Define CRS for plot. This makes it easier to change the CRS without\n# re-running the entire code\nepsg_plot = epsg_wgs #5070, 2163\n\nfig, ax = plt.subplots(1, 1, figsize=(8,8))\nbnd.to_crs(epsg_plot).plot(ax=ax, facecolor='None', edgecolor='k', linewidth=2)\nstations.to_crs(epsg_plot).plot(ax=ax, color='k', marker='v')\nvoronoi_poly.to_crs(epsg_plot).plot(ax=ax, facecolor='None')\nvoronoi_poly.loc[[0]].to_crs(epsg_plot).plot(ax=ax, hatch='//', facecolor='None')\n\nvertices.to_crs(epsg_plot).plot(ax=ax, marker='o', markersize=15, color='tomato')\n\n# Add LEC (note that the centroid is the geometry)\nlec.to_crs(epsg_plot).plot(ax=ax, marker='x', facecolor='k', markersize=100)\n\n# Change dataframe geometry to plot the circle boundary\nlec.set_geometry('circle_coords', crs=epsg_wgs).to_crs(epsg_plot).plot(ax=ax, \n facecolor=(0.9,0.5,0.5,0.2), \n edgecolor='k')\n\nplt.title('Largest empty circle')\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/largest_empty_circle.html#references", + "href": "exercises/largest_empty_circle.html#references", + "title": "83  Largest empty circle", + "section": "References", + "text": "References\nPatrignani, A., Mohankumar, N., Redmond, C., Santos, E. A., & Knapp, M. (2020). Optimizing the spatial configuration of mesoscale environmental monitoring networks using a geometric approach. Journal of Atmospheric and Oceanic Technology, 37(5), 943-956." + }, + { + "objectID": "exercises/weather_network_coverage.html", + "href": "exercises/weather_network_coverage.html", + "title": "84  Weather network coverage", + "section": "", + "text": "85 Find area not represented by the stations\n# Find non-represented county areas\nnon_represented_area = counties.overlay(stations, how='difference')\nnon_represented_area.plot(alpha=0.5, edgecolor='k', cmap='tab10');\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#define-geographic-and-projected-coordinate-reference-systems", + "href": "exercises/weather_network_coverage.html#define-geographic-and-projected-coordinate-reference-systems", + "title": "84  Weather network coverage", + "section": "Define geographic and projected coordinate reference systems", + "text": "Define geographic and projected coordinate reference systems\nA Coordinate Reference System (CRS) is a framework that defines how the two-dimensional, flat surface of a map relates to the real places on the three-dimensional surface of the Earth. CRSs ensure that locations are accurately represented in spatial analyses and visualizations. There are two primary types of CRSs: Geographic Coordinate Systems and Projected Coordinate Systems.\nGeographic Coordinate Systems use a three-dimensional spherical surface to define locations on the Earth. They express locations as latitude and longitude, which are angles measured from the Earth’s center to a point on the Earth’s surface. This system is great for global and regional mapping because it represents the Earth’s curvature.\nOn the other hand, Projected Coordinate Systems transform geographic coordinates into Cartesian coordinates (x and y values) on a flat surface. This projection process introduces some distortions, but it allows for more practical measurements and calculations in units like meters, making projected coordinates ideal for detailed local maps where accurate distances and areas are essential. Each PCS is designed for specific regions or purposes, minimizing distortions within its intended area of use. You will notice that computations of area and length, and even the determinatino of buffers in GeoPandas require changing the CRS from geographic to projected coordinates.\n\n# Define projected and geographic coordinate reference systems\nutm14 = 32614 # UTM Zone 14\nwgs84 = 4326 # WGS84" + }, + { + "objectID": "exercises/weather_network_coverage.html#read-and-inspect-datasets", + "href": "exercises/weather_network_coverage.html#read-and-inspect-datasets", + "title": "84  Weather network coverage", + "section": "Read and inspect datasets", + "text": "Read and inspect datasets\n\n# Read stations\nstations = gpd.read_file('../datasets/KS_mesonet_geoinfo.csv')\nstations[['lon','lat']] = stations[['lon','lat']].astype('float')\nstations['geometry'] = gpd.points_from_xy(x=stations['lon'], y=stations['lat'], crs=wgs84)\n\n\n# Read counties map (only for visuals, not used in any core computation) (already in WGS84)\ncounties = gpd.read_file('../datasets/spatial/us_county_5m.geojson') #.to_crs(epsg_wgs)\nidx_state = counties['STATE_NAME'] == 'Kansas'\ncounties = counties.loc[idx_state].reset_index(drop=True)\n\n\n# Creaet figure using object-based syntax\nfig, ax = plt.subplots(1, 1, figsize=(6,4))\ncounties.plot(ax=ax, facecolor='None', edgecolor='gray', linewidth=0.25)\nstations.plot(ax=ax, facecolor='tomato', edgecolor='k', markersize=5)\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#create-buffer", + "href": "exercises/weather_network_coverage.html#create-buffer", + "title": "84  Weather network coverage", + "section": "Create buffer", + "text": "Create buffer\nWe will assume that each weather station is representative of an area with a 50 km radius. While on a daily basis locations within this buffer area can exhibit weather differenes, overall climatic trends should be fairly consistent within this radius.\n\n# Create a buffer\nstations['buffer'] = stations.to_crs(utm14).buffer(50_000).to_crs(wgs84)\n\n# Set buffer as geometry for easy plotting and analysis in posterior steps\nstations.set_geometry('buffer', inplace=True)\n\n\n# Creaet figure using object-based syntax\nfig, ax = plt.subplots(1, 1, figsize=(6,4))\ncounties.plot(ax=ax, facecolor='None', edgecolor='gray', linewidth=0.5)\nstations.plot(ax=ax, facecolor='None', edgecolor='k')\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#find-area-represented-by-the-stations", + "href": "exercises/weather_network_coverage.html#find-area-represented-by-the-stations", + "title": "84  Weather network coverage", + "section": "Find area represented by the stations", + "text": "Find area represented by the stations\n\n# Find represented areas of the state\nrepresented_area = counties.overlay(stations, how='intersection')\nrepresented_area.plot(alpha=0.5, edgecolor='k', cmap='tab10');\nplt.xlabel('Longitude')\nplt.xlabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#dissolve-polygons", + "href": "exercises/weather_network_coverage.html#dissolve-polygons", + "title": "84  Weather network coverage", + "section": "Dissolve polygons", + "text": "Dissolve polygons\nNote that the previous polygons still retain the county boundaries. If we want to compute the area of each polygon, we first need to dissolve the internal boundaries. This operation will create a single MultiPolygon feature with all the polygons inside. To separate each polygon, we can use the explode() method.\n\n# Dissolve polygons\nnon_represented_area = non_represented_area.dissolve()\nnon_represented_area = non_represented_area.explode(index_parts=True).reset_index(drop=True)\nnon_represented_area.plot(alpha=0.5, edgecolor='k', cmap='tab10');\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#find-largest-polygon-not-covered-by-the-network", + "href": "exercises/weather_network_coverage.html#find-largest-polygon-not-covered-by-the-network", + "title": "84  Weather network coverage", + "section": "Find largest polygon not covered by the network", + "text": "Find largest polygon not covered by the network\n\n# Find largest non-represented area\nnon_represented_area['area'] = non_represented_area.to_crs(utm14).area\nnon_represented_area['perimeter'] = non_represented_area.to_crs(utm14).length\nidx_largest_area = non_represented_area['area'].argmax()\nnon_represented_area.head(3)\n\n\n\n\n\n\n\n\nSTATEFP\nCOUNTYFP\nCOUNTYNS\nAFFGEOID\nGEOID\nNAME\nNAMELSAD\nSTUSPS\nSTATE_NAME\nLSAD\nALAND\nAWATER\narea\nperimeter\ngeometry\n\n\n\n\n0\n20\n137\n00485032\n0500000US20137\n20137\nNorton\nNorton County\nKS\nKansas\n06\n2274122718\n8421464\n3.187139e+09\n300298.876438\nPOLYGON ((-99.37539 37.00018, -99.40702 36.999...\n\n\n1\n20\n137\n00485032\n0500000US20137\n20137\nNorton\nNorton County\nKS\nKansas\n06\n2274122718\n8421464\n1.542991e+07\n21123.638265\nPOLYGON ((-98.30280 37.53727, -98.29914 37.535...\n\n\n2\n20\n137\n00485032\n0500000US20137\n20137\nNorton\nNorton County\nKS\nKansas\n06\n2274122718\n8421464\n1.996688e+07\n24035.816406\nPOLYGON ((-99.30669 38.04489, -99.31468 38.001..." + }, + { + "objectID": "exercises/weather_network_coverage.html#display-the-largest-area-not-represented-by-the-network", + "href": "exercises/weather_network_coverage.html#display-the-largest-area-not-represented-by-the-network", + "title": "84  Weather network coverage", + "section": "Display the largest area not represented by the network", + "text": "Display the largest area not represented by the network\n\n# Find non-represented county areas\n\n# We need to restore geometry to points (not buffers)\nstations.set_geometry('geometry',inplace=True)\n\n# Creaet figure showing largest non-represented area\nfig, ax = plt.subplots(1, 1, figsize=(6,4))\ncounties.plot(ax=ax, facecolor='None', edgecolor='gray', linewidth=0.25)\nnon_represented_area.plot(ax=ax, alpha=0.5, edgecolor='k', cmap='tab10');\nstations.plot(ax=ax, facecolor='tomato', edgecolor='k', markersize=5)\nnon_represented_area.loc[[idx_largest_area],'geometry'].plot(ax=ax, facecolor='None',edgecolor='red')\nplt.xlabel('Longitude')\nplt.ylabel('Latitude')\nplt.show()" + }, + { + "objectID": "exercises/weather_network_coverage.html#practice", + "href": "exercises/weather_network_coverage.html#practice", + "title": "84  Weather network coverage", + "section": "Practice", + "text": "Practice\n\nHow do the non-represented areas change in shape and size when changing the buffer radius from 50 km to 10 km?\nUsing the area and perimeter of the polygons representing areas without station coverage, compute additional metrics to identify larger polygons with different shape properties. One option is to use the Shape Index, SI = \\frac{Perimeter}{\\sqrt{Area}}, which decreases as the area increases, for a given perimeter. This makes it a suitable metric for identifying large polygons without being overly penalized for having a larger perimeter." + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#define-helper-functions", + "href": "exercises/estimate_daily_vapor_pressure.html#define-helper-functions", + "title": "85  Estimate daily vapor pressure", + "section": "Define helper functions", + "text": "Define helper functions\n\n# Define helper functions for vapor pressure\nfn_e_sat = lambda T: 0.6108 * np.exp(17.27*T/(T+237.3)) # Saturation vapor pressure\nfn_e_act = lambda T,RH: fn_e_sat(T) * RH/100 # Actual vapor pressure" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#read-and-inspect-dataset", + "href": "exercises/estimate_daily_vapor_pressure.html#read-and-inspect-dataset", + "title": "85  Estimate daily vapor pressure", + "section": "Read and inspect dataset", + "text": "Read and inspect dataset\n\n# Read 5-minute dataset\ndf = pd.read_csv('../datasets/garden_city_5min.csv', parse_dates=['TIMESTAMP'])\n\n# Drop unnnecessary columns\ndf = df.drop(['PRESSUREAVG','PRECIP','SRAVG','WSPD2MAVG'], axis='columns')\n\n# Rename columns with shorter names\ndf.rename(columns={'TEMP2MAVG':'tavg', 'RELHUM2MAVG':'rhavg'}, inplace=True)\n\n# Set datetime column as timeindex\ndf.set_index('TIMESTAMP', inplace=True)\n\n# Inspect a few rows\ndf.head(3)\n\n\n\n\n\n\n\n\ntavg\nrhavg\n\n\nTIMESTAMP\n\n\n\n\n\n\n2019-01-01 00:00:00\n-10.74\n80.01\n\n\n2019-01-01 00:05:00\n-10.68\n77.15\n\n\n2019-01-01 00:10:00\n-10.67\n78.70" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#filter-dataset", + "href": "exercises/estimate_daily_vapor_pressure.html#filter-dataset", + "title": "85  Estimate daily vapor pressure", + "section": "Filter dataset", + "text": "Filter dataset\nThis step will help us remove a few outliers. A Savitzky-Golay filter using a short window and a third-order polynomial will retain much of the temporal changes, while also filtering out spurious data.\n\n# Apply filter to remove possible spurious observations in the 5-min data\ndf['tavg'] = savgol_filter(df['tavg'], 11, 3)\ndf['rhavg'] = savgol_filter(df['rhavg'], 11, 3)" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#compute-variables-at-5-minute-intervals", + "href": "exercises/estimate_daily_vapor_pressure.html#compute-variables-at-5-minute-intervals", + "title": "85  Estimate daily vapor pressure", + "section": "Compute variables at 5-minute intervals", + "text": "Compute variables at 5-minute intervals\nThese data will serve as the basis for the daily benchmark values.\n\n# Compute saturation vapor pressure using 5-min observations\ndf['e_sat'] = fn_e_sat(df['tavg'])\n\n# Compute actual vapor pressure using 5-min observations\ndf['e_act'] = fn_e_act(df['tavg'], df['rhavg'])\n\n# Compute vapor pressure deficit using 5-min observations\ndf['vpd'] = df['e_sat'] - df['e_act']" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#resample-from-5-minute-to-hourly", + "href": "exercises/estimate_daily_vapor_pressure.html#resample-from-5-minute-to-hourly", + "title": "85  Estimate daily vapor pressure", + "section": "Resample from 5-minute to hourly", + "text": "Resample from 5-minute to hourly\n\n# Compute daily values\ndf_daily = df.resample('1D').agg(tmin=('tavg','min'),\n tmax=('tavg','max'),\n rhmin=('rhavg','min'),\n rhmax=('rhavg','max'),\n e_sat=('e_sat','mean'),\n e_act=('e_act','mean'),\n vpd=('vpd','mean'))\n\n\n# Compute daily average saturation vapor pressure using tmin and tmax\ne_sat_min = fn_e_sat(df_daily['tmin']) \ne_sat_max = fn_e_sat(df_daily['tmax']) \ndf_daily['e_sat_minmax'] = (e_sat_min + e_sat_max)/2\n\n# Compute actual vapor pressure using daily values tmin, tmax, rhmin, and rhmax\ndf_daily['e_act_minmax'] = (e_sat_min * df_daily['rhmax']/100 + \\\n e_sat_max * df_daily['rhmin']/100)/2\n\n# Compute daily average vapor pressure deficit\ndf_daily['vpd_minmax'] = df_daily['e_sat_minmax'] - df_daily['e_act_minmax']\n\n\n\n# Create figure\nplt.figure(figsize=(8,3))\nplt.plot(df_daily['vpd'], color='k', label='Mean VPD using 5-min data')\nplt.plot(df_daily['vpd_minmax'], color='tomato', alpha=0.5,\n label='VPD from Tmin, Tmax, RHmin, and RHmax')\nplt.xlabel('Time')\nplt.ylabel('Predicted vpd (kPa)')\nplt.legend()\nplt.show()\n\n\n\n\n\nplt.figure(figsize=(10,3))\n\nplt.subplot(1,3,1)\nplt.scatter(df_daily['e_sat'], df_daily['e_sat_minmax'],\n facecolor='w', edgecolor='k', alpha=0.5, label='Obs')\nplt.plot([0,6],[0,6], '-k', label='1:1 line')\nplt.xlim([0,6])\nplt.ylim([0,6])\nplt.xlabel('Benchmark e_sat (kPa)')\nplt.ylabel('Predicted e_sat (kPa)')\nplt.legend()\n\nplt.subplot(1,3,2)\nplt.title('Estimations using Tmin, Tmax, RHmin, and RHmax')\nplt.scatter(df_daily['e_act'], df_daily['e_act_minmax'],\n facecolor='w', edgecolor='k', alpha=0.5,)\nplt.plot([0,5],[0,5], '-k')\nplt.xlim([0,5])\nplt.ylim([0,5])\nplt.xlabel('Benchmark e_act (kPa)')\nplt.ylabel('Predicted e_act (kPa)')\n\nplt.subplot(1,3,3)\nplt.scatter(df_daily['vpd'], df_daily['vpd_minmax'],\n facecolor='w', edgecolor='k', alpha=0.5,)\nplt.plot([0,5],[0,5], '-k')\nplt.xlim([0,5])\nplt.ylim([0,5])\nplt.xlabel('Benchmark vpd (kPa)')\nplt.ylabel('Predicted vpd (kPa)')\n\nplt.subplots_adjust(wspace=0.3)\nplt.show()" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#adjust-vpd-using-a-linear-model", + "href": "exercises/estimate_daily_vapor_pressure.html#adjust-vpd-using-a-linear-model", + "title": "85  Estimate daily vapor pressure", + "section": "Adjust VPD using a linear model", + "text": "Adjust VPD using a linear model\nOne of the simplest alternatives to correct the bias in VPD values is to use a linear model. Based on visual inspection, it seems that we only need to correct the slope.\n\n# Regression for saturation vapor pressure\nX = df_daily['vpd_minmax']\ny = df_daily['vpd']\nvpd_ols_model = sm.OLS(y, X).fit()\nvpd_ols_model.summary()\n\n\nOLS Regression Results\n\n\nDep. Variable:\nvpd\nR-squared (uncentered):\n0.986\n\n\nModel:\nOLS\nAdj. R-squared (uncentered):\n0.986\n\n\nMethod:\nLeast Squares\nF-statistic:\n1.059e+05\n\n\nDate:\nSun, 04 Feb 2024\nProb (F-statistic):\n0.00\n\n\nTime:\n00:47:19\nLog-Likelihood:\n838.72\n\n\nNo. Observations:\n1461\nAIC:\n-1675.\n\n\nDf Residuals:\n1460\nBIC:\n-1670.\n\n\nDf Model:\n1\n\n\n\n\nCovariance Type:\nnonrobust\n\n\n\n\n\n\n\n\n\ncoef\nstd err\nt\nP>|t|\n[0.025\n0.975]\n\n\nvpd_minmax\n0.8313\n0.003\n325.400\n0.000\n0.826\n0.836\n\n\n\n\n\n\nOmnibus:\n164.099\nDurbin-Watson:\n1.283\n\n\nProb(Omnibus):\n0.000\nJarque-Bera (JB):\n1370.115\n\n\nSkew:\n0.112\nProb(JB):\n3.04e-298\n\n\nKurtosis:\n7.739\nCond. No.\n1.00\n\n\n\nNotes:[1] R² is computed without centering (uncentered) since the model does not contain a constant.[2] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n\n\n\n# Compute corrected vpd\ndf_daily['vpd_adj'] = vpd_ols_model.predict(df_daily['vpd_minmax'])\n\n\nplt.figure(figsize=(3,3))\nplt.title('Adjusted VPD using OLS')\nplt.scatter(df_daily['vpd'],df_daily['vpd_adj'],\n facecolor='w', edgecolor='k', alpha=0.5)\nplt.plot([0,5],[0,5], '-k')\nplt.xlim([0,5])\nplt.ylim([0,5])\nplt.xlabel('Benchmark vpd (kPa)')\nplt.ylabel('Predicted vpd (kPa)')\nplt.show()" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#machine-learning", + "href": "exercises/estimate_daily_vapor_pressure.html#machine-learning", + "title": "85  Estimate daily vapor pressure", + "section": "Machine learning", + "text": "Machine learning\nIn this part we will explore the determination of e_sat, e_act, and vpd simultaneaously using a machine learning model. Note that vpd could be compute as the difference of the other two variablles, but to illustrate the power of machine learning models and match the benchmark data from 5-minute observations we will estimate all three variables.\n\n# Create dataset\nX = df_daily[['tmin','tmax','rhmin','rhmax']]\ny = df_daily[['e_sat','e_act','vpd']]\ny.head()\n\n\n\n\n\n\n\n\ne_sat\ne_act\nvpd\n\n\nTIMESTAMP\n\n\n\n\n\n\n\n2019-01-01\n0.257936\n0.166566\n0.091370\n\n\n2019-01-02\n0.367207\n0.241662\n0.125546\n\n\n2019-01-03\n0.432865\n0.314997\n0.117868\n\n\n2019-01-04\n0.665950\n0.477658\n0.188293\n\n\n2019-01-05\n0.806039\n0.579511\n0.226528\n\n\n\n\n\n\n\n\n# Split dataset\nX_train, X_test, y_train, y_test = train_test_split(X, y, random_state=1)\n\n\n# Normalize data\nscaler = StandardScaler() \n\n# Scale training data\nscaler.fit(X_train) \nX_train = scaler.transform(X_train) \n\n# Apply same transformation to test data\nX_test = scaler.transform(X_test) \n\n\n# Create and fit multilayer perceptron (MLP)\nMLP = MLPRegressor(random_state=1, max_iter=500, hidden_layer_sizes=(100,), \n activation='relu', solver='adam')\n\n# Fit MLP to training data\nMLP_fitted = MLP.fit(X_train, y_train)\n\n\n# Run trained MLP model on the test set\npred_test = MLP_fitted.predict(X_test)\n\n\n# Compute the coefficient of determination\nMLP_fitted.score(X_test, y_test)\n\n0.9740855079562776\n\n\n\n# Create scatter plots comparing test set and MLP predictions\nplt.figure(figsize=(10,3))\n\nplt.subplot(1,3,1)\nplt.scatter(y_test['e_sat'], pred_test[:,0], \n facecolor='w', edgecolor='k', alpha=0.5, label='Obs')\nplt.plot([0,6],[0,6], '-k', label='1:1 line')\nplt.xlim([0,6])\nplt.ylim([0,6])\nplt.xlabel('Benchmark e_sat (kPa)')\nplt.ylabel('Predicted e_sat (kPa)')\nplt.legend()\n\nplt.subplot(1,3,2)\nplt.title('Machine Learning Predictions')\nplt.scatter(y_test['e_act'], pred_test[:,1],\n facecolor='w', edgecolor='k', alpha=0.5, )\nplt.plot([0,5],[0,5], '-k')\nplt.xlim([0,5])\nplt.ylim([0,5])\nplt.xlabel('Benchmark e_act (kPa)')\nplt.ylabel('Predicted e_act (kPa)')\n\nplt.subplot(1,3,3)\nplt.scatter(y_test['vpd'], pred_test[:,2],\n facecolor='w', edgecolor='k', alpha=0.5, )\nplt.plot([0,5],[0,5], '-k')\nplt.xlim([0,5])\nplt.ylim([0,5])\nplt.xlabel('Benchmark vpd (kPa)')\nplt.ylabel('Predicted vpd (kPa)')\n\nplt.subplots_adjust(wspace=0.3)\nplt.show()" + }, + { + "objectID": "exercises/estimate_daily_vapor_pressure.html#practice", + "href": "exercises/estimate_daily_vapor_pressure.html#practice", + "title": "85  Estimate daily vapor pressure", + "section": "Practice", + "text": "Practice\n\nCompute the mean absolute error and the mean bias error for the different methods.\nLoad the 5-minute dataset for Ashland Bottoms, KS and compute e_sat, e_act, and vpd with the fitted linear model and the machine learning model. Does it work for another locations without any additional calibration?" + } +] \ No newline at end of file diff --git a/docs/search.json b/docs/search.json index cfb2af4..8a471d9 100644 --- a/docs/search.json +++ b/docs/search.json @@ -1870,7 +1870,7 @@ "href": "exercises/multiple_linear_regression.html", "title": "61  Multiple linear regression", "section": "", - "text": "Multiple linear regression analysis is a statistical technique used to model the relationship between two or more independent variables (x) and a single dependent variable (y). By fitting a linear equation to observed data, this method allows for the prediction of the dependent variable based on the values of the independent variables. It extends simple linear regression, which involves only one independent variable, to include multiple factors, providing a more comprehensive understanding of complex relationships within the data. The general formula is: y = \\beta_0 \\ + \\beta_1 \\ x_1 + ... + \\beta_n \\ x_n\nAn agronomic example that involves the use of multiple linear regression are allometric measurements, such as estimating corn biomass based on plant height and stem diameter.\n\n# Import modules\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport statsmodels.api as sm\n\n\n# Read dataset\ndf = pd.read_csv(\"../datasets/corn_allometric_biomass.csv\")\ndf.head(3)\n\n\n\n\n\n\n\n\nheight_cm\nstem_diam_mm\ndry_biomass_g\n\n\n\n\n0\n71.0\n5.7\n0.66\n\n\n1\n39.0\n4.4\n0.19\n\n\n2\n55.5\n4.3\n0.30\n\n\n\n\n\n\n\n\n# Re-define variables for better plot semantics and shorter variable names\nx_data = df['stem_diam_mm'].values\ny_data = df['height_cm'].values\nz_data = df['dry_biomass_g'].values\n\n\n# Plot raw data using 3D plots.\n# Great tutorial: https://jakevdp.github.io/PythonDataScienceHandbook/04.12-three-dimensional-plotting.html\n\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n#ax = fig.add_subplot(projection='3d')\n\n\n# Define axess\nax.scatter3D(x_data, y_data, z_data, c='r');\nax.set_xlabel('Stem diameter (mm)') \nax.set_ylabel('Plant height (cm)')\nax.set_zlabel('Dry biomass (g)')\n\nax.view_init(elev=20, azim=100)\nplt.show()\n\n# elev=None, azim=None\n# elev = elevation angle in the z plane.\n# azim = stores the azimuth angle in the x,y plane.\n\n\n\n\n\n# Full model\n\n# Create array of intercept values\n# We can also use X = sm.add_constant(X)\nintercept = np.ones(df.shape[0])\n \n# Create matrix with inputs (rows represent obseravtions and columns the variables)\nX = np.column_stack((intercept, \n x_data,\n y_data,\n x_data * y_data)) # interaction term\n\n# Print a few rows\nprint(X[0:3,:])\n\n[[ 1. 5.7 71. 404.7 ]\n [ 1. 4.4 39. 171.6 ]\n [ 1. 4.3 55.5 238.65]]\n\n\n\n# Run Ordinary Least Squares to fit the model\nmodel = sm.OLS(z_data, X)\nresults = model.fit()\nprint(results.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.849\nModel: OLS Adj. R-squared: 0.836\nMethod: Least Squares F-statistic: 63.71\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 4.87e-14\nTime: 14:12:19 Log-Likelihood: -129.26\nNo. Observations: 38 AIC: 266.5\nDf Residuals: 34 BIC: 273.1\nDf Model: 3 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 18.8097 6.022 3.124 0.004 6.572 31.048\nx1 -4.5537 1.222 -3.727 0.001 -7.037 -2.070\nx2 -0.1830 0.119 -1.541 0.133 -0.424 0.058\nx3 0.0433 0.007 6.340 0.000 0.029 0.057\n==============================================================================\nOmnibus: 9.532 Durbin-Watson: 2.076\nProb(Omnibus): 0.009 Jarque-Bera (JB): 9.232\nSkew: 0.861 Prob(JB): 0.00989\nKurtosis: 4.692 Cond. No. 1.01e+04\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 1.01e+04. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nheight (x1) does not seem to be statistically significant. This term has a p-value > 0.05 and the range of the 95% confidence interval for its corresponding \\beta coefficient includes zero.\nThe goal is to prune the full model by removing non-significant terms. After removing these terms, we need to fit the model again to update the new coefficients.\n\n# Define prunned model\nX_prunned = np.column_stack((intercept, \n x_data, \n x_data * y_data))\n\n# Re-fit the model\nmodel_prunned = sm.OLS(z_data, X_prunned)\nresults_prunned = model_prunned.fit()\nprint(results_prunned.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.838\nModel: OLS Adj. R-squared: 0.829\nMethod: Least Squares F-statistic: 90.81\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 1.40e-14\nTime: 14:13:05 Log-Likelihood: -130.54\nNo. Observations: 38 AIC: 267.1\nDf Residuals: 35 BIC: 272.0\nDf Model: 2 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 14.0338 5.263 2.666 0.012 3.349 24.719\nx1 -5.0922 1.194 -4.266 0.000 -7.515 -2.669\nx2 0.0367 0.005 6.761 0.000 0.026 0.048\n==============================================================================\nOmnibus: 12.121 Durbin-Watson: 2.194\nProb(Omnibus): 0.002 Jarque-Bera (JB): 13.505\nSkew: 0.997 Prob(JB): 0.00117\nKurtosis: 5.135 Cond. No. 8.80e+03\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 8.8e+03. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nThe prunned model has: - r-squared remains similar - one less parameter - higher F-Statistic 91 vs 63 - AIC remains similar (the lower the better)\n\n# Access parameter/coefficient values\nprint(results_prunned.params)\n\n[14.03378102 -5.09215874 0.03671944]\n\n\n\n# Create surface grid\n\n# Xgrid is grid of stem diameter\nx_vec = np.linspace(x_data.min(), x_data.max(), 21)\n\n# Ygrid is grid of plant height\ny_vec = np.linspace(y_data.min(), y_data.max(), 21)\n\n# We generate a 2D grid\nX_grid, Y_grid = np.meshgrid(x_vec, y_vec)\n\n# Create intercept grid\nintercept = np.ones(X_grid.shape)\n\n# Get parameter values\npars = results_prunned.params\n\n# Z is the elevation of this 2D grid\nZ_grid = intercept*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]\n\nAlternatively you can use the .predict() method of the fitted object. This option would required flattening the arrays to make predictions:\nX_pred = np.column_stack((intercept.flatten(), X_grid.flatten(), X_grid.flatten() * Y_grid.flatten()) )\nZ_grid = model_prunned.predict(params=results_prunned.params, exog=X_pred)\nZ_grid = np.reshape(Z_grid, X_grid.shape) # Reset shape to match \n\n# Plot points with predicted model (which is a surface)\n\n# Create figure and axes\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n\nax.scatter3D(x_data, y_data, z_data, c='r', s=80);\nsurf = ax.plot_wireframe(X_grid, Y_grid, Z_grid, color='black')\n#surf = ax.plot_surface(Xgrid, Ygrid, Zgrid, cmap='green', rstride=1, cstride=1)\n\nax.set_xlabel('Stem diameter (mm)')\nax.set_ylabel('Plant height x stem diameter')\nax.set_zlabel('Dry biomass (g)')\nax.view_init(20, 130)\nfig.tight_layout()\nax.set_box_aspect(aspect=None, zoom=0.8) # Zoom out to see zlabel\nax.set_zlim([0,100])\nplt.show()\n\n\n\n\n\n\n# We can now create a lambda function to help use convert other observations\ncorn_biomass_fn = lambda stem,height: pars[0] + stem*pars[1] + stem*height*pars[2]\n\n\n# Test the lambda function using stem = 11 mm and height = 120 cm\nbiomass = corn_biomass_fn(11, 120)\nprint(round(biomass,2), 'g')\n\n6.49 g" + "text": "Multiple linear regression analysis is a statistical technique used to model the relationship between two or more independent variables (x) and a single dependent variable (y). By fitting a linear equation to observed data, this method allows for the prediction of the dependent variable based on the values of the independent variables. It extends simple linear regression, which involves only one independent variable, to include multiple factors, providing a more comprehensive understanding of complex relationships within the data. The general formula is: y = \\beta_0 \\ + \\beta_1 \\ x_1 + ... + \\beta_n \\ x_n\nAn agronomic example that involves the use of multiple linear regression are allometric measurements, such as estimating corn biomass based on plant height and stem diameter.\n\n# Import modules\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport statsmodels.api as sm\n\n\n# Read dataset\ndf = pd.read_csv(\"../datasets/corn_allometric_biomass.csv\")\ndf.head(3)\n\n\n\n\n\n\n\n\nheight_cm\nstem_diam_mm\ndry_biomass_g\n\n\n\n\n0\n71.0\n5.7\n0.66\n\n\n1\n39.0\n4.4\n0.19\n\n\n2\n55.5\n4.3\n0.30\n\n\n\n\n\n\n\n\n# Re-define variables for better plot semantics and shorter variable names\nx_data = df['stem_diam_mm'].values\ny_data = df['height_cm'].values\nz_data = df['dry_biomass_g'].values\n\n\n# Plot raw data using 3D plots.\n# Great tutorial: https://jakevdp.github.io/PythonDataScienceHandbook/04.12-three-dimensional-plotting.html\n\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n#ax = fig.add_subplot(projection='3d')\n\n\n# Define axess\nax.scatter3D(x_data, y_data, z_data, c='r');\nax.set_xlabel('Stem diameter (mm)') \nax.set_ylabel('Plant height (cm)')\nax.set_zlabel('Dry biomass (g)')\n\nax.view_init(elev=20, azim=100)\nplt.show()\n\n# elev=None, azim=None\n# elev = elevation angle in the z plane.\n# azim = stores the azimuth angle in the x,y plane.\n\n\n\n\n\n# Full model\n\n# Create array of intercept values\n# We can also use X = sm.add_constant(X)\nintercept = np.ones(df.shape[0])\n \n# Create matrix with inputs (rows represent obseravtions and columns the variables)\nX = np.column_stack((intercept, \n x_data,\n y_data,\n x_data * y_data)) # interaction term\n\n# Print a few rows\nprint(X[0:3,:])\n\n[[ 1. 5.7 71. 404.7 ]\n [ 1. 4.4 39. 171.6 ]\n [ 1. 4.3 55.5 238.65]]\n\n\n\n# Run Ordinary Least Squares to fit the model\nmodel = sm.OLS(z_data, X)\nresults = model.fit()\nprint(results.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.849\nModel: OLS Adj. R-squared: 0.836\nMethod: Least Squares F-statistic: 63.71\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 4.87e-14\nTime: 14:12:19 Log-Likelihood: -129.26\nNo. Observations: 38 AIC: 266.5\nDf Residuals: 34 BIC: 273.1\nDf Model: 3 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 18.8097 6.022 3.124 0.004 6.572 31.048\nx1 -4.5537 1.222 -3.727 0.001 -7.037 -2.070\nx2 -0.1830 0.119 -1.541 0.133 -0.424 0.058\nx3 0.0433 0.007 6.340 0.000 0.029 0.057\n==============================================================================\nOmnibus: 9.532 Durbin-Watson: 2.076\nProb(Omnibus): 0.009 Jarque-Bera (JB): 9.232\nSkew: 0.861 Prob(JB): 0.00989\nKurtosis: 4.692 Cond. No. 1.01e+04\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 1.01e+04. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nheight (x1) does not seem to be statistically significant. This term has a p-value > 0.05 and the range of the 95% confidence interval for its corresponding \\beta coefficient includes zero.\nThe goal is to prune the full model by removing non-significant terms. After removing these terms, we need to fit the model again to update the new coefficients.\n\n# Define prunned model\nX_prunned = np.column_stack((intercept, \n x_data, \n x_data * y_data))\n\n# Re-fit the model\nmodel_prunned = sm.OLS(z_data, X_prunned)\nresults_prunned = model_prunned.fit()\nprint(results_prunned.summary())\n\n OLS Regression Results \n==============================================================================\nDep. Variable: y R-squared: 0.838\nModel: OLS Adj. R-squared: 0.829\nMethod: Least Squares F-statistic: 90.81\nDate: Wed, 27 Mar 2024 Prob (F-statistic): 1.40e-14\nTime: 14:13:05 Log-Likelihood: -130.54\nNo. Observations: 38 AIC: 267.1\nDf Residuals: 35 BIC: 272.0\nDf Model: 2 \nCovariance Type: nonrobust \n==============================================================================\n coef std err t P>|t| [0.025 0.975]\n------------------------------------------------------------------------------\nconst 14.0338 5.263 2.666 0.012 3.349 24.719\nx1 -5.0922 1.194 -4.266 0.000 -7.515 -2.669\nx2 0.0367 0.005 6.761 0.000 0.026 0.048\n==============================================================================\nOmnibus: 12.121 Durbin-Watson: 2.194\nProb(Omnibus): 0.002 Jarque-Bera (JB): 13.505\nSkew: 0.997 Prob(JB): 0.00117\nKurtosis: 5.135 Cond. No. 8.80e+03\n==============================================================================\n\nNotes:\n[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.\n[2] The condition number is large, 8.8e+03. This might indicate that there are\nstrong multicollinearity or other numerical problems.\n\n\nThe prunned model has: - r-squared remains similar - one less parameter - higher F-Statistic 91 vs 63 - AIC remains similar (the lower the better)\n\n# Access parameter/coefficient values\nprint(results_prunned.params)\n\n[14.03378102 -5.09215874 0.03671944]\n\n\n\n# Create surface grid\n\n# Xgrid is grid of stem diameter\nx_vec = np.linspace(x_data.min(), x_data.max(), 21)\n\n# Ygrid is grid of plant height\ny_vec = np.linspace(y_data.min(), y_data.max(), 21)\n\n# We generate a 2D grid\nX_grid, Y_grid = np.meshgrid(x_vec, y_vec)\n\n# Create intercept grid\nintercept_grid = np.ones(X_grid.shape)\n\n# Get parameter values\npars = results_prunned.params\n\n# Z is the elevation of this 2D grid\nZ_grid = intercept_grid*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]\n\nAlternatively you can use the .predict() method of the fitted object. This option would required flattening the arrays to make predictions:\nX_pred = np.column_stack((intercept.flatten(), X_grid.flatten(), X_grid.flatten() * Y_grid.flatten()) )\nZ_grid = model_prunned.predict(params=results_prunned.params, exog=X_pred)\nZ_grid = np.reshape(Z_grid, X_grid.shape) # Reset shape to match \n\n# Plot points with predicted model (which is a surface)\n\n# Create figure and axes\nfig = plt.figure(figsize=(5,5))\nax = plt.axes(projection='3d')\n\nax.scatter3D(x_data, y_data, z_data, c='r', s=80);\nsurf = ax.plot_wireframe(X_grid, Y_grid, Z_grid, color='black')\n#surf = ax.plot_surface(Xgrid, Ygrid, Zgrid, cmap='green', rstride=1, cstride=1)\n\nax.set_xlabel('Stem diameter (mm)')\nax.set_ylabel('Plant height x stem diameter')\nax.set_zlabel('Dry biomass (g)')\nax.view_init(20, 130)\nfig.tight_layout()\nax.set_box_aspect(aspect=None, zoom=0.8) # Zoom out to see zlabel\nax.set_zlim([0,100])\nplt.show()\n\n\n\n\n\n\n# We can now create a lambda function to help use convert other observations\ncorn_biomass_fn = lambda stem,height: pars[0] + stem*pars[1] + stem*height*pars[2]\n\n\n# Test the lambda function using stem = 11 mm and height = 120 cm\nbiomass = corn_biomass_fn(11, 120)\nprint(round(biomass,2), 'g')\n\n6.49 g" }, { "objectID": "exercises/proctor_test.html#references", diff --git a/docs/site_libs/bootstrap/bootstrap.min (Andres Patrignani's conflicted copy 2024-03-27).css b/docs/site_libs/bootstrap/bootstrap.min (Andres Patrignani's conflicted copy 2024-03-27).css new file mode 100644 index 0000000..e7bad80 --- /dev/null +++ b/docs/site_libs/bootstrap/bootstrap.min (Andres Patrignani's conflicted copy 2024-03-27).css @@ -0,0 +1,10 @@ +/*! + * Bootstrap v5.1.3 (https://getbootstrap.com/) + * Copyright 2011-2021 The Bootstrap Authors + * Copyright 2011-2021 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@300;400;700&display=swap";:root{--bs-blue: #2780e3;--bs-indigo: #6610f2;--bs-purple: #613d7c;--bs-pink: #e83e8c;--bs-red: #ff0039;--bs-orange: #f0ad4e;--bs-yellow: #ff7518;--bs-green: #3fb618;--bs-teal: #20c997;--bs-cyan: #9954bb;--bs-white: #fff;--bs-gray: #6c757d;--bs-gray-dark: #373a3c;--bs-gray-100: #f8f9fa;--bs-gray-200: #e9ecef;--bs-gray-300: #dee2e6;--bs-gray-400: #ced4da;--bs-gray-500: #adb5bd;--bs-gray-600: #6c757d;--bs-gray-700: #495057;--bs-gray-800: #373a3c;--bs-gray-900: #212529;--bs-default: #373a3c;--bs-primary: #2780e3;--bs-secondary: #373a3c;--bs-success: #3fb618;--bs-info: #9954bb;--bs-warning: #ff7518;--bs-danger: #ff0039;--bs-light: #f8f9fa;--bs-dark: #373a3c;--bs-default-rgb: 55, 58, 60;--bs-primary-rgb: 39, 128, 227;--bs-secondary-rgb: 55, 58, 60;--bs-success-rgb: 63, 182, 24;--bs-info-rgb: 153, 84, 187;--bs-warning-rgb: 255, 117, 24;--bs-danger-rgb: 255, 0, 57;--bs-light-rgb: 248, 249, 250;--bs-dark-rgb: 55, 58, 60;--bs-white-rgb: 255, 255, 255;--bs-black-rgb: 0, 0, 0;--bs-body-color-rgb: 55, 58, 60;--bs-body-bg-rgb: 255, 255, 255;--bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-root-font-size: 17px;--bs-body-font-family: var(--bs-font-sans-serif);--bs-body-font-size: 1rem;--bs-body-font-weight: 400;--bs-body-line-height: 1.5;--bs-body-color: #373a3c;--bs-body-bg: #fff}*,*::before,*::after{box-sizing:border-box}:root{font-size:var(--bs-root-font-size)}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h6,.h6,h5,.h5,h4,.h4,h3,.h3,h2,.h2,h1,.h1{margin-top:0;margin-bottom:.5rem;font-weight:400;line-height:1.2}h1,.h1{font-size:calc(1.325rem + 0.9vw)}@media(min-width: 1200px){h1,.h1{font-size:2rem}}h2,.h2{font-size:calc(1.29rem + 0.48vw)}@media(min-width: 1200px){h2,.h2{font-size:1.65rem}}h3,.h3{font-size:calc(1.27rem + 0.24vw)}@media(min-width: 1200px){h3,.h3{font-size:1.45rem}}h4,.h4{font-size:1.25rem}h5,.h5{font-size:1.1rem}h6,.h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-bs-original-title]{text-decoration:underline dotted;-webkit-text-decoration:underline dotted;-moz-text-decoration:underline dotted;-ms-text-decoration:underline dotted;-o-text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem;padding:.625rem 1.25rem;border-left:.25rem solid #e9ecef}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}b,strong{font-weight:bolder}small,.small{font-size:0.875em}mark,.mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:0.75em;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}a{color:#2780e3;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}a:hover{color:#1f66b6}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr /* rtl:ignore */;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:0.875em;color:#000;background-color:#f7f7f7;padding:.5rem;border:1px solid #dee2e6}pre code{background-color:rgba(0,0,0,0);font-size:inherit;color:inherit;word-break:normal}code{font-size:0.875em;color:#9753b8;background-color:#f7f7f7;padding:.125rem .25rem;word-wrap:break-word}a>code{color:inherit}kbd{padding:.4rem .4rem;font-size:0.875em;color:#fff;background-color:#212529}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}thead,tbody,tfoot,tr,td,th{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button:not(:disabled),[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + 0.3vw);line-height:inherit}@media(min-width: 1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-text,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none !important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media(min-width: 1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:0.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:0.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:0.875em;color:#6c757d}.grid{display:grid;grid-template-rows:repeat(var(--bs-rows, 1), 1fr);grid-template-columns:repeat(var(--bs-columns, 12), 1fr);gap:var(--bs-gap, 1.5rem)}.grid .g-col-1{grid-column:auto/span 1}.grid .g-col-2{grid-column:auto/span 2}.grid .g-col-3{grid-column:auto/span 3}.grid .g-col-4{grid-column:auto/span 4}.grid .g-col-5{grid-column:auto/span 5}.grid .g-col-6{grid-column:auto/span 6}.grid .g-col-7{grid-column:auto/span 7}.grid .g-col-8{grid-column:auto/span 8}.grid .g-col-9{grid-column:auto/span 9}.grid .g-col-10{grid-column:auto/span 10}.grid .g-col-11{grid-column:auto/span 11}.grid .g-col-12{grid-column:auto/span 12}.grid .g-start-1{grid-column-start:1}.grid .g-start-2{grid-column-start:2}.grid .g-start-3{grid-column-start:3}.grid .g-start-4{grid-column-start:4}.grid .g-start-5{grid-column-start:5}.grid .g-start-6{grid-column-start:6}.grid .g-start-7{grid-column-start:7}.grid .g-start-8{grid-column-start:8}.grid .g-start-9{grid-column-start:9}.grid .g-start-10{grid-column-start:10}.grid .g-start-11{grid-column-start:11}@media(min-width: 576px){.grid .g-col-sm-1{grid-column:auto/span 1}.grid .g-col-sm-2{grid-column:auto/span 2}.grid .g-col-sm-3{grid-column:auto/span 3}.grid .g-col-sm-4{grid-column:auto/span 4}.grid .g-col-sm-5{grid-column:auto/span 5}.grid .g-col-sm-6{grid-column:auto/span 6}.grid .g-col-sm-7{grid-column:auto/span 7}.grid .g-col-sm-8{grid-column:auto/span 8}.grid .g-col-sm-9{grid-column:auto/span 9}.grid .g-col-sm-10{grid-column:auto/span 10}.grid .g-col-sm-11{grid-column:auto/span 11}.grid .g-col-sm-12{grid-column:auto/span 12}.grid .g-start-sm-1{grid-column-start:1}.grid .g-start-sm-2{grid-column-start:2}.grid .g-start-sm-3{grid-column-start:3}.grid .g-start-sm-4{grid-column-start:4}.grid .g-start-sm-5{grid-column-start:5}.grid .g-start-sm-6{grid-column-start:6}.grid .g-start-sm-7{grid-column-start:7}.grid .g-start-sm-8{grid-column-start:8}.grid .g-start-sm-9{grid-column-start:9}.grid .g-start-sm-10{grid-column-start:10}.grid .g-start-sm-11{grid-column-start:11}}@media(min-width: 768px){.grid .g-col-md-1{grid-column:auto/span 1}.grid .g-col-md-2{grid-column:auto/span 2}.grid .g-col-md-3{grid-column:auto/span 3}.grid .g-col-md-4{grid-column:auto/span 4}.grid .g-col-md-5{grid-column:auto/span 5}.grid .g-col-md-6{grid-column:auto/span 6}.grid .g-col-md-7{grid-column:auto/span 7}.grid .g-col-md-8{grid-column:auto/span 8}.grid .g-col-md-9{grid-column:auto/span 9}.grid .g-col-md-10{grid-column:auto/span 10}.grid .g-col-md-11{grid-column:auto/span 11}.grid .g-col-md-12{grid-column:auto/span 12}.grid .g-start-md-1{grid-column-start:1}.grid .g-start-md-2{grid-column-start:2}.grid .g-start-md-3{grid-column-start:3}.grid .g-start-md-4{grid-column-start:4}.grid .g-start-md-5{grid-column-start:5}.grid .g-start-md-6{grid-column-start:6}.grid .g-start-md-7{grid-column-start:7}.grid .g-start-md-8{grid-column-start:8}.grid .g-start-md-9{grid-column-start:9}.grid .g-start-md-10{grid-column-start:10}.grid .g-start-md-11{grid-column-start:11}}@media(min-width: 992px){.grid .g-col-lg-1{grid-column:auto/span 1}.grid .g-col-lg-2{grid-column:auto/span 2}.grid .g-col-lg-3{grid-column:auto/span 3}.grid .g-col-lg-4{grid-column:auto/span 4}.grid .g-col-lg-5{grid-column:auto/span 5}.grid .g-col-lg-6{grid-column:auto/span 6}.grid .g-col-lg-7{grid-column:auto/span 7}.grid .g-col-lg-8{grid-column:auto/span 8}.grid .g-col-lg-9{grid-column:auto/span 9}.grid .g-col-lg-10{grid-column:auto/span 10}.grid .g-col-lg-11{grid-column:auto/span 11}.grid .g-col-lg-12{grid-column:auto/span 12}.grid .g-start-lg-1{grid-column-start:1}.grid .g-start-lg-2{grid-column-start:2}.grid .g-start-lg-3{grid-column-start:3}.grid .g-start-lg-4{grid-column-start:4}.grid .g-start-lg-5{grid-column-start:5}.grid .g-start-lg-6{grid-column-start:6}.grid .g-start-lg-7{grid-column-start:7}.grid .g-start-lg-8{grid-column-start:8}.grid .g-start-lg-9{grid-column-start:9}.grid .g-start-lg-10{grid-column-start:10}.grid .g-start-lg-11{grid-column-start:11}}@media(min-width: 1200px){.grid .g-col-xl-1{grid-column:auto/span 1}.grid .g-col-xl-2{grid-column:auto/span 2}.grid .g-col-xl-3{grid-column:auto/span 3}.grid .g-col-xl-4{grid-column:auto/span 4}.grid .g-col-xl-5{grid-column:auto/span 5}.grid .g-col-xl-6{grid-column:auto/span 6}.grid .g-col-xl-7{grid-column:auto/span 7}.grid .g-col-xl-8{grid-column:auto/span 8}.grid .g-col-xl-9{grid-column:auto/span 9}.grid .g-col-xl-10{grid-column:auto/span 10}.grid .g-col-xl-11{grid-column:auto/span 11}.grid .g-col-xl-12{grid-column:auto/span 12}.grid .g-start-xl-1{grid-column-start:1}.grid .g-start-xl-2{grid-column-start:2}.grid .g-start-xl-3{grid-column-start:3}.grid .g-start-xl-4{grid-column-start:4}.grid .g-start-xl-5{grid-column-start:5}.grid .g-start-xl-6{grid-column-start:6}.grid .g-start-xl-7{grid-column-start:7}.grid .g-start-xl-8{grid-column-start:8}.grid .g-start-xl-9{grid-column-start:9}.grid .g-start-xl-10{grid-column-start:10}.grid .g-start-xl-11{grid-column-start:11}}@media(min-width: 1400px){.grid .g-col-xxl-1{grid-column:auto/span 1}.grid .g-col-xxl-2{grid-column:auto/span 2}.grid .g-col-xxl-3{grid-column:auto/span 3}.grid .g-col-xxl-4{grid-column:auto/span 4}.grid .g-col-xxl-5{grid-column:auto/span 5}.grid .g-col-xxl-6{grid-column:auto/span 6}.grid .g-col-xxl-7{grid-column:auto/span 7}.grid .g-col-xxl-8{grid-column:auto/span 8}.grid .g-col-xxl-9{grid-column:auto/span 9}.grid .g-col-xxl-10{grid-column:auto/span 10}.grid .g-col-xxl-11{grid-column:auto/span 11}.grid .g-col-xxl-12{grid-column:auto/span 12}.grid .g-start-xxl-1{grid-column-start:1}.grid .g-start-xxl-2{grid-column-start:2}.grid .g-start-xxl-3{grid-column-start:3}.grid .g-start-xxl-4{grid-column-start:4}.grid .g-start-xxl-5{grid-column-start:5}.grid .g-start-xxl-6{grid-column-start:6}.grid .g-start-xxl-7{grid-column-start:7}.grid .g-start-xxl-8{grid-column-start:8}.grid .g-start-xxl-9{grid-column-start:9}.grid .g-start-xxl-10{grid-column-start:10}.grid .g-start-xxl-11{grid-column-start:11}}.table{--bs-table-bg: transparent;--bs-table-accent-bg: transparent;--bs-table-striped-color: #373a3c;--bs-table-striped-bg: rgba(0, 0, 0, 0.05);--bs-table-active-color: #373a3c;--bs-table-active-bg: rgba(0, 0, 0, 0.1);--bs-table-hover-color: #373a3c;--bs-table-hover-bg: rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#373a3c;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:first-child){border-top:2px solid #b6babc}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-accent-bg: var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg: var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover>*{--bs-table-accent-bg: var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg: #d4e6f9;--bs-table-striped-bg: #c9dbed;--bs-table-striped-color: #000;--bs-table-active-bg: #bfcfe0;--bs-table-active-color: #000;--bs-table-hover-bg: #c4d5e6;--bs-table-hover-color: #000;color:#000;border-color:#bfcfe0}.table-secondary{--bs-table-bg: #d7d8d8;--bs-table-striped-bg: #cccdcd;--bs-table-striped-color: #000;--bs-table-active-bg: #c2c2c2;--bs-table-active-color: #000;--bs-table-hover-bg: #c7c8c8;--bs-table-hover-color: #000;color:#000;border-color:#c2c2c2}.table-success{--bs-table-bg: #d9f0d1;--bs-table-striped-bg: #cee4c7;--bs-table-striped-color: #000;--bs-table-active-bg: #c3d8bc;--bs-table-active-color: #000;--bs-table-hover-bg: #c9dec1;--bs-table-hover-color: #000;color:#000;border-color:#c3d8bc}.table-info{--bs-table-bg: #ebddf1;--bs-table-striped-bg: #dfd2e5;--bs-table-striped-color: #000;--bs-table-active-bg: #d4c7d9;--bs-table-active-color: #000;--bs-table-hover-bg: #d9ccdf;--bs-table-hover-color: #000;color:#000;border-color:#d4c7d9}.table-warning{--bs-table-bg: #ffe3d1;--bs-table-striped-bg: #f2d8c7;--bs-table-striped-color: #000;--bs-table-active-bg: #e6ccbc;--bs-table-active-color: #000;--bs-table-hover-bg: #ecd2c1;--bs-table-hover-color: #000;color:#000;border-color:#e6ccbc}.table-danger{--bs-table-bg: #ffccd7;--bs-table-striped-bg: #f2c2cc;--bs-table-striped-color: #000;--bs-table-active-bg: #e6b8c2;--bs-table-active-color: #000;--bs-table-hover-bg: #ecbdc7;--bs-table-hover-color: #000;color:#000;border-color:#e6b8c2}.table-light{--bs-table-bg: #f8f9fa;--bs-table-striped-bg: #ecedee;--bs-table-striped-color: #000;--bs-table-active-bg: #dfe0e1;--bs-table-active-color: #000;--bs-table-hover-bg: #e5e6e7;--bs-table-hover-color: #000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg: #373a3c;--bs-table-striped-bg: #414446;--bs-table-striped-color: #fff;--bs-table-active-bg: #4b4e50;--bs-table-active-color: #fff;--bs-table-hover-bg: #46494b;--bs-table-hover-color: #fff;color:#fff;border-color:#4b4e50}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media(max-width: 575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media(max-width: 1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label,.shiny-input-container .control-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.875rem}.form-text{margin-top:.25rem;font-size:0.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#373a3c;background-color:#fff;border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#373a3c;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-0.375rem -0.75rem;margin-inline-end:.75rem;color:#373a3c;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-control::-webkit-file-upload-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#373a3c;background-color:rgba(0,0,0,0);border:solid rgba(0,0,0,0);border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + 0.5rem + 2px);padding:.25rem .5rem;font-size:0.875rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-0.25rem -0.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-0.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + 0.75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + 0.5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em}.form-control-color::-webkit-color-swatch{height:1.5em}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:0;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-select{transition:none}}.form-select:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:rgba(0,0,0,0);text-shadow:0 0 0 #373a3c}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:0.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check,.shiny-input-container .checkbox,.shiny-input-container .radio{display:block;min-height:1.5rem;padding-left:0;margin-bottom:.125rem}.form-check .form-check-input,.form-check .shiny-input-container .checkbox input,.form-check .shiny-input-container .radio input,.shiny-input-container .checkbox .form-check-input,.shiny-input-container .checkbox .shiny-input-container .checkbox input,.shiny-input-container .checkbox .shiny-input-container .radio input,.shiny-input-container .radio .form-check-input,.shiny-input-container .radio .shiny-input-container .checkbox input,.shiny-input-container .radio .shiny-input-container .radio input{float:left;margin-left:0}.form-check-input,.shiny-input-container .checkbox input,.shiny-input-container .checkbox-inline input,.shiny-input-container .radio input,.shiny-input-container .radio-inline input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none;color-adjust:exact;-webkit-print-color-adjust:exact}.form-check-input[type=radio],.shiny-input-container .checkbox input[type=radio],.shiny-input-container .checkbox-inline input[type=radio],.shiny-input-container .radio input[type=radio],.shiny-input-container .radio-inline input[type=radio]{border-radius:50%}.form-check-input:active,.shiny-input-container .checkbox input:active,.shiny-input-container .checkbox-inline input:active,.shiny-input-container .radio input:active,.shiny-input-container .radio-inline input:active{filter:brightness(90%)}.form-check-input:focus,.shiny-input-container .checkbox input:focus,.shiny-input-container .checkbox-inline input:focus,.shiny-input-container .radio input:focus,.shiny-input-container .radio-inline input:focus{border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.form-check-input:checked,.shiny-input-container .checkbox input:checked,.shiny-input-container .checkbox-inline input:checked,.shiny-input-container .radio input:checked,.shiny-input-container .radio-inline input:checked{background-color:#2780e3;border-color:#2780e3}.form-check-input:checked[type=checkbox],.shiny-input-container .checkbox input:checked[type=checkbox],.shiny-input-container .checkbox-inline input:checked[type=checkbox],.shiny-input-container .radio input:checked[type=checkbox],.shiny-input-container .radio-inline input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio],.shiny-input-container .checkbox input:checked[type=radio],.shiny-input-container .checkbox-inline input:checked[type=radio],.shiny-input-container .radio input:checked[type=radio],.shiny-input-container .radio-inline input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate,.shiny-input-container .checkbox input[type=checkbox]:indeterminate,.shiny-input-container .checkbox-inline input[type=checkbox]:indeterminate,.shiny-input-container .radio input[type=checkbox]:indeterminate,.shiny-input-container .radio-inline input[type=checkbox]:indeterminate{background-color:#2780e3;border-color:#2780e3;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled,.shiny-input-container .checkbox input:disabled,.shiny-input-container .checkbox-inline input:disabled,.shiny-input-container .radio input:disabled,.shiny-input-container .radio-inline input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input[disabled]~.form-check-label,.form-check-input[disabled]~span,.form-check-input:disabled~.form-check-label,.form-check-input:disabled~span,.shiny-input-container .checkbox input[disabled]~.form-check-label,.shiny-input-container .checkbox input[disabled]~span,.shiny-input-container .checkbox input:disabled~.form-check-label,.shiny-input-container .checkbox input:disabled~span,.shiny-input-container .checkbox-inline input[disabled]~.form-check-label,.shiny-input-container .checkbox-inline input[disabled]~span,.shiny-input-container .checkbox-inline input:disabled~.form-check-label,.shiny-input-container .checkbox-inline input:disabled~span,.shiny-input-container .radio input[disabled]~.form-check-label,.shiny-input-container .radio input[disabled]~span,.shiny-input-container .radio input:disabled~.form-check-label,.shiny-input-container .radio input:disabled~span,.shiny-input-container .radio-inline input[disabled]~.form-check-label,.shiny-input-container .radio-inline input[disabled]~span,.shiny-input-container .radio-inline input:disabled~.form-check-label,.shiny-input-container .radio-inline input:disabled~span{opacity:.5}.form-check-label,.shiny-input-container .checkbox label,.shiny-input-container .checkbox-inline label,.shiny-input-container .radio label,.shiny-input-container .radio-inline label{cursor:pointer}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;transition:background-position .15s ease-in-out}@media(prefers-reduced-motion: reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2393c0f1'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline,.shiny-input-container .checkbox-inline,.shiny-input-container .radio-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.btn-check[disabled]+.btn,.btn-check:disabled+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:rgba(0,0,0,0);appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(39,128,227,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#bed9f7}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#dee2e6;border-color:rgba(0,0,0,0)}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#2780e3;border:0;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none;-o-appearance:none}@media(prefers-reduced-motion: reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#bed9f7}.form-range::-moz-range-track{width:100%;height:.5rem;color:rgba(0,0,0,0);cursor:pointer;background-color:#dee2e6;border-color:rgba(0,0,0,0)}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid rgba(0,0,0,0);transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media(prefers-reduced-motion: reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::placeholder{color:rgba(0,0,0,0)}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(0.85) translateY(-0.5rem) translateX(0.15rem)}.input-group{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:stretch;-webkit-align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#373a3c;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da}.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text,.input-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem}.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text,.input-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#3fb618}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:rgba(63,182,24,.9)}.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip,.is-valid~.valid-feedback,.is-valid~.valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#3fb618;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:valid,.form-select.is-valid{border-color:#3fb618}.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"],.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%233fb618' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:valid:focus,.form-select.is-valid:focus{border-color:#3fb618;box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid,.form-check-input.is-valid{border-color:#3fb618}.was-validated .form-check-input:valid:checked,.form-check-input.is-valid:checked{background-color:#3fb618}.was-validated .form-check-input:valid:focus,.form-check-input.is-valid:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.25)}.was-validated .form-check-input:valid~.form-check-label,.form-check-input.is-valid~.form-check-label{color:#3fb618}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.was-validated .input-group .form-control:valid,.input-group .form-control.is-valid,.was-validated .input-group .form-select:valid,.input-group .form-select.is-valid{z-index:1}.was-validated .input-group .form-control:valid:focus,.input-group .form-control.is-valid:focus,.was-validated .input-group .form-select:valid:focus,.input-group .form-select.is-valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:0.875em;color:#ff0039}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:0.875rem;color:#fff;background-color:rgba(255,0,57,.9)}.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip,.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#ff0039;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .form-select:invalid,.form-select.is-invalid{border-color:#ff0039}.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"],.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23373a3c' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff0039'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff0039' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-select:invalid:focus,.form-select.is-invalid:focus{border-color:#ff0039;box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid,.form-check-input.is-invalid{border-color:#ff0039}.was-validated .form-check-input:invalid:checked,.form-check-input.is-invalid:checked{background-color:#ff0039}.was-validated .form-check-input:invalid:focus,.form-check-input.is-invalid:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.25)}.was-validated .form-check-input:invalid~.form-check-label,.form-check-input.is-invalid~.form-check-label{color:#ff0039}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.was-validated .input-group .form-control:invalid,.input-group .form-control.is-invalid,.was-validated .input-group .form-select:invalid,.input-group .form-select.is-invalid{z-index:2}.was-validated .input-group .form-control:invalid:focus,.input-group .form-control.is-invalid:focus,.was-validated .input-group .form-select:invalid:focus,.input-group .form-select.is-invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#373a3c;text-align:center;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);padding:.375rem .75rem;font-size:1rem;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#373a3c}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.btn:disabled,.btn.disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-default{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-default:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-default,.btn-default:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-default,.btn-check:active+.btn-default,.btn-default:active,.btn-default.active,.show>.btn-default.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-default:focus,.btn-check:active+.btn-default:focus,.btn-default:active:focus,.btn-default.active:focus,.show>.btn-default.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-default:disabled,.btn-default.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-primary{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-primary:hover{color:#fff;background-color:#216dc1;border-color:#1f66b6}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#216dc1;border-color:#1f66b6;box-shadow:0 0 0 .25rem rgba(71,147,231,.5)}.btn-check:checked+.btn-primary,.btn-check:active+.btn-primary,.btn-primary:active,.btn-primary.active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#1f66b6;border-color:#1d60aa}.btn-check:checked+.btn-primary:focus,.btn-check:active+.btn-primary:focus,.btn-primary:active:focus,.btn-primary.active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(71,147,231,.5)}.btn-primary:disabled,.btn-primary.disabled{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-secondary{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-secondary:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-secondary,.btn-check:active+.btn-secondary,.btn-secondary:active,.btn-secondary.active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-secondary:focus,.btn-check:active+.btn-secondary:focus,.btn-secondary:active:focus,.btn-secondary.active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-secondary:disabled,.btn-secondary.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-success{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-success:hover{color:#fff;background-color:#369b14;border-color:#329213}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#369b14;border-color:#329213;box-shadow:0 0 0 .25rem rgba(92,193,59,.5)}.btn-check:checked+.btn-success,.btn-check:active+.btn-success,.btn-success:active,.btn-success.active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#329213;border-color:#2f8912}.btn-check:checked+.btn-success:focus,.btn-check:active+.btn-success:focus,.btn-success:active:focus,.btn-success.active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(92,193,59,.5)}.btn-success:disabled,.btn-success.disabled{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-info{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-info:hover{color:#fff;background-color:#82479f;border-color:#7a4396}.btn-check:focus+.btn-info,.btn-info:focus{color:#fff;background-color:#82479f;border-color:#7a4396;box-shadow:0 0 0 .25rem rgba(168,110,197,.5)}.btn-check:checked+.btn-info,.btn-check:active+.btn-info,.btn-info:active,.btn-info.active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#7a4396;border-color:#733f8c}.btn-check:checked+.btn-info:focus,.btn-check:active+.btn-info:focus,.btn-info:active:focus,.btn-info.active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(168,110,197,.5)}.btn-info:disabled,.btn-info.disabled{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-warning{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-warning:hover{color:#fff;background-color:#d96314;border-color:#cc5e13}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#fff;background-color:#d96314;border-color:#cc5e13;box-shadow:0 0 0 .25rem rgba(255,138,59,.5)}.btn-check:checked+.btn-warning,.btn-check:active+.btn-warning,.btn-warning:active,.btn-warning.active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#cc5e13;border-color:#bf5812}.btn-check:checked+.btn-warning:focus,.btn-check:active+.btn-warning:focus,.btn-warning:active:focus,.btn-warning.active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(255,138,59,.5)}.btn-warning:disabled,.btn-warning.disabled{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-danger{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-danger:hover{color:#fff;background-color:#d90030;border-color:#cc002e}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#d90030;border-color:#cc002e;box-shadow:0 0 0 .25rem rgba(255,38,87,.5)}.btn-check:checked+.btn-danger,.btn-check:active+.btn-danger,.btn-danger:active,.btn-danger.active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#cc002e;border-color:#bf002b}.btn-check:checked+.btn-danger:focus,.btn-check:active+.btn-danger:focus,.btn-danger:active:focus,.btn-danger.active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(255,38,87,.5)}.btn-danger:disabled,.btn-danger.disabled{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:checked+.btn-light,.btn-check:active+.btn-light,.btn-light:active,.btn-light.active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:checked+.btn-light:focus,.btn-check:active+.btn-light:focus,.btn-light:active:focus,.btn-light.active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light:disabled,.btn-light.disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-dark:hover{color:#fff;background-color:#2f3133;border-color:#2c2e30}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#2f3133;border-color:#2c2e30;box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-check:checked+.btn-dark,.btn-check:active+.btn-dark,.btn-dark:active,.btn-dark.active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#2c2e30;border-color:#292c2d}.btn-check:checked+.btn-dark:focus,.btn-check:active+.btn-dark:focus,.btn-dark:active:focus,.btn-dark.active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(85,88,89,.5)}.btn-dark:disabled,.btn-dark.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-outline-default{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-default:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-default,.btn-outline-default:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-default,.btn-check:active+.btn-outline-default,.btn-outline-default:active,.btn-outline-default.active,.btn-outline-default.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-default:focus,.btn-check:active+.btn-outline-default:focus,.btn-outline-default:active:focus,.btn-outline-default.active:focus,.btn-outline-default.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-default:disabled,.btn-outline-default.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-primary{color:#2780e3;border-color:#2780e3;background-color:rgba(0,0,0,0)}.btn-outline-primary:hover{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(39,128,227,.5)}.btn-check:checked+.btn-outline-primary,.btn-check:active+.btn-outline-primary,.btn-outline-primary:active,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show{color:#fff;background-color:#2780e3;border-color:#2780e3}.btn-check:checked+.btn-outline-primary:focus,.btn-check:active+.btn-outline-primary:focus,.btn-outline-primary:active:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(39,128,227,.5)}.btn-outline-primary:disabled,.btn-outline-primary.disabled{color:#2780e3;background-color:rgba(0,0,0,0)}.btn-outline-secondary{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-secondary:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-secondary,.btn-check:active+.btn-outline-secondary,.btn-outline-secondary:active,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-secondary:focus,.btn-check:active+.btn-outline-secondary:focus,.btn-outline-secondary:active:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-secondary:disabled,.btn-outline-secondary.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-success{color:#3fb618;border-color:#3fb618;background-color:rgba(0,0,0,0)}.btn-outline-success:hover{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.5)}.btn-check:checked+.btn-outline-success,.btn-check:active+.btn-outline-success,.btn-outline-success:active,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show{color:#fff;background-color:#3fb618;border-color:#3fb618}.btn-check:checked+.btn-outline-success:focus,.btn-check:active+.btn-outline-success:focus,.btn-outline-success:active:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(63,182,24,.5)}.btn-outline-success:disabled,.btn-outline-success.disabled{color:#3fb618;background-color:rgba(0,0,0,0)}.btn-outline-info{color:#9954bb;border-color:#9954bb;background-color:rgba(0,0,0,0)}.btn-outline-info:hover{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(153,84,187,.5)}.btn-check:checked+.btn-outline-info,.btn-check:active+.btn-outline-info,.btn-outline-info:active,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show{color:#fff;background-color:#9954bb;border-color:#9954bb}.btn-check:checked+.btn-outline-info:focus,.btn-check:active+.btn-outline-info:focus,.btn-outline-info:active:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(153,84,187,.5)}.btn-outline-info:disabled,.btn-outline-info.disabled{color:#9954bb;background-color:rgba(0,0,0,0)}.btn-outline-warning{color:#ff7518;border-color:#ff7518;background-color:rgba(0,0,0,0)}.btn-outline-warning:hover{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,117,24,.5)}.btn-check:checked+.btn-outline-warning,.btn-check:active+.btn-outline-warning,.btn-outline-warning:active,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show{color:#fff;background-color:#ff7518;border-color:#ff7518}.btn-check:checked+.btn-outline-warning:focus,.btn-check:active+.btn-outline-warning:focus,.btn-outline-warning:active:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(255,117,24,.5)}.btn-outline-warning:disabled,.btn-outline-warning.disabled{color:#ff7518;background-color:rgba(0,0,0,0)}.btn-outline-danger{color:#ff0039;border-color:#ff0039;background-color:rgba(0,0,0,0)}.btn-outline-danger:hover{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.5)}.btn-check:checked+.btn-outline-danger,.btn-check:active+.btn-outline-danger,.btn-outline-danger:active,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show{color:#fff;background-color:#ff0039;border-color:#ff0039}.btn-check:checked+.btn-outline-danger:focus,.btn-check:active+.btn-outline-danger:focus,.btn-outline-danger:active:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(255,0,57,.5)}.btn-outline-danger:disabled,.btn-outline-danger.disabled{color:#ff0039;background-color:rgba(0,0,0,0)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:checked+.btn-outline-light,.btn-check:active+.btn-outline-light,.btn-outline-light:active,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:checked+.btn-outline-light:focus,.btn-check:active+.btn-outline-light:focus,.btn-outline-light:active:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light:disabled,.btn-outline-light.disabled{color:#f8f9fa;background-color:rgba(0,0,0,0)}.btn-outline-dark{color:#373a3c;border-color:#373a3c;background-color:rgba(0,0,0,0)}.btn-outline-dark:hover{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-check:checked+.btn-outline-dark,.btn-check:active+.btn-outline-dark,.btn-outline-dark:active,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show{color:#fff;background-color:#373a3c;border-color:#373a3c}.btn-check:checked+.btn-outline-dark:focus,.btn-check:active+.btn-outline-dark:focus,.btn-outline-dark:active:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus{box-shadow:0 0 0 .25rem rgba(55,58,60,.5)}.btn-outline-dark:disabled,.btn-outline-dark.disabled{color:#373a3c;background-color:rgba(0,0,0,0)}.btn-link{font-weight:400;color:#2780e3;text-decoration:underline;-webkit-text-decoration:underline;-moz-text-decoration:underline;-ms-text-decoration:underline;-o-text-decoration:underline}.btn-link:hover{color:#1f66b6}.btn-link:disabled,.btn-link.disabled{color:#6c757d}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:0.875rem;border-radius:0}.fade{transition:opacity .15s linear}@media(prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .2s ease}@media(prefers-reduced-motion: reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media(prefers-reduced-motion: reduce){.collapsing.collapse-horizontal{transition:none}}.dropup,.dropend,.dropdown,.dropstart{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid rgba(0,0,0,0);border-bottom:0;border-left:.3em solid rgba(0,0,0,0)}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#373a3c;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position: start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position: end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media(min-width: 576px){.dropdown-menu-sm-start{--bs-position: start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position: end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 768px){.dropdown-menu-md-start{--bs-position: start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position: end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 992px){.dropdown-menu-lg-start{--bs-position: start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position: end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1200px){.dropdown-menu-xl-start{--bs-position: start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position: end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media(min-width: 1400px){.dropdown-menu-xxl-start{--bs-position: start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position: end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid rgba(0,0,0,0);border-bottom:.3em solid;border-left:.3em solid rgba(0,0,0,0)}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:0;border-bottom:.3em solid rgba(0,0,0,0);border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid rgba(0,0,0,0);border-right:.3em solid;border-bottom:.3em solid rgba(0,0,0,0)}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap;background-color:rgba(0,0,0,0);border:0}.dropdown-item:hover,.dropdown-item:focus{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#2780e3}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:rgba(0,0,0,0)}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:0.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#373a3c;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:hover,.dropdown-menu-dark .dropdown-item:focus{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#2780e3}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto}.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn:hover,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;justify-content:flex-start;-webkit-justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;-webkit-flex-direction:column;align-items:flex-start;-webkit-align-items:flex-start;justify-content:center;-webkit-justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.nav{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#2780e3;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media(prefers-reduced-motion: reduce){.nav-link{transition:none}}.nav-link:hover,.nav-link:focus{color:#1f66b6}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:none;border:1px solid rgba(0,0,0,0)}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:rgba(0,0,0,0);border-color:rgba(0,0,0,0)}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px}.nav-pills .nav-link{background:none;border:0}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#2780e3}.nav-fill>.nav-link,.nav-fill .nav-item{flex:1 1 auto;-webkit-flex:1 1 auto;text-align:center}.nav-justified>.nav-link,.nav-justified .nav-item{flex-basis:0;-webkit-flex-basis:0;flex-grow:1;-webkit-flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container-xxl,.navbar>.container-xl,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container,.navbar>.container-fluid{display:flex;display:-webkit-flex;flex-wrap:inherit;-webkit-flex-wrap:inherit;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;-webkit-flex-basis:100%;flex-grow:1;-webkit-flex-grow:1;align-items:center;-webkit-align-items:center}.navbar-toggler{padding:.25 0;font-size:1.25rem;line-height:1;background-color:rgba(0,0,0,0);border:1px solid rgba(0,0,0,0);transition:box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height, 75vh);overflow-y:auto}@media(min-width: 576px){.navbar-expand-sm{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-top,.navbar-expand-sm .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 768px){.navbar-expand-md{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-top,.navbar-expand-md .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 992px){.navbar-expand-lg{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-top,.navbar-expand-lg .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1200px){.navbar-expand-xl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-top,.navbar-expand-xl .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}@media(min-width: 1400px){.navbar-expand-xxl{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-top,.navbar-expand-xxl .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;-webkit-flex-wrap:nowrap;justify-content:flex-start;-webkit-justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row;-webkit-flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex !important;display:-webkit-flex !important;flex-basis:auto;-webkit-flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;-webkit-flex-grow:1;visibility:visible !important;background-color:rgba(0,0,0,0);border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-top,.navbar-expand .offcanvas-bottom{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;display:-webkit-flex;flex-grow:0;-webkit-flex-grow:0;padding:0;overflow-y:visible}.navbar-light{background-color:#f8f9fa}.navbar-light .navbar-brand{color:#545555}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#1a5698}.navbar-light .navbar-nav .nav-link{color:#545555}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:rgba(26,86,152,.8)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(84,85,85,.75)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .nav-link.active{color:#1a5698}.navbar-light .navbar-toggler{color:#545555;border-color:rgba(84,85,85,0)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23545555' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:#545555}.navbar-light .navbar-text a,.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#1a5698}.navbar-dark{background-color:#f8f9fa}.navbar-dark .navbar-brand{color:#545555}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#1a5698}.navbar-dark .navbar-nav .nav-link{color:#545555}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:rgba(26,86,152,.8)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(84,85,85,.75)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active{color:#1a5698}.navbar-dark .navbar-toggler{color:#545555;border-color:rgba(84,85,85,0)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23545555' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:#545555}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#1a5698}.card{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0}.card>.list-group:last-child{border-bottom-width:0}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-0.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:#adb5bd;border-bottom:1px solid rgba(0,0,0,.125)}.card-footer{padding:.5rem 1rem;background-color:#adb5bd;border-top:1px solid rgba(0,0,0,.125)}.card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem}.card-img,.card-img-top,.card-img-bottom{width:100%}.card-group>.card{margin-bottom:.75rem}@media(min-width: 576px){.card-group{display:flex;display:-webkit-flex;flex-flow:row wrap;-webkit-flex-flow:row wrap}.card-group>.card{flex:1 0 0%;-webkit-flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}}.accordion-button{position:relative;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#373a3c;text-align:left;background-color:#fff;border:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media(prefers-reduced-motion: reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#2373cc;background-color:#e9f2fc;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%232373cc'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;-webkit-flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23373a3c'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media(prefers-reduced-motion: reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#93c0f1;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:not(:first-of-type){border-top:0}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.breadcrumb{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, ">") /* rtl: var(--bs-breadcrumb-divider, ">") */}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;display:-webkit-flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#2780e3;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media(prefers-reduced-motion: reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#1f66b6;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#1f66b6;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#2780e3;border-color:#2780e3}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:0.875rem}.badge{display:inline-block;padding:.35em .65em;font-size:0.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:0 solid rgba(0,0,0,0)}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-default{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-default .alert-link{color:#1a1c1d}.alert-primary{color:#174d88;background-color:#d4e6f9;border-color:#bed9f7}.alert-primary .alert-link{color:#123e6d}.alert-secondary{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-secondary .alert-link{color:#1a1c1d}.alert-success{color:#266d0e;background-color:#d9f0d1;border-color:#c5e9ba}.alert-success .alert-link{color:#1e570b}.alert-info{color:#5c3270;background-color:#ebddf1;border-color:#e0cceb}.alert-info .alert-link{color:#4a285a}.alert-warning{color:#99460e;background-color:#ffe3d1;border-color:#ffd6ba}.alert-warning .alert-link{color:#7a380b}.alert-danger{color:#902;background-color:#ffccd7;border-color:#ffb3c4}.alert-danger .alert-link{color:#7a001b}.alert-light{color:#959596;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#777778}.alert-dark{color:#212324;background-color:#d7d8d8;border-color:#c3c4c5}.alert-dark .alert-link{color:#1a1c1d}@keyframes progress-bar-stripes{0%{background-position-x:.5rem}}.progress{display:flex;display:-webkit-flex;height:.5rem;overflow:hidden;font-size:0.75rem;background-color:#e9ecef}.progress-bar{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;justify-content:center;-webkit-justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#2780e3;transition:width .6s ease}@media(prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-size:.5rem .5rem}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media(prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.list-group{display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;padding-left:0;margin-bottom:0}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#373a3c;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;-webkit-text-decoration:none;-moz-text-decoration:none;-ms-text-decoration:none;-o-text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#2780e3;border-color:#2780e3}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media(min-width: 576px){.list-group-horizontal-sm{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 768px){.list-group-horizontal-md{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 992px){.list-group-horizontal-lg{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 1200px){.list-group-horizontal-xl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media(min-width: 1400px){.list-group-horizontal-xxl{flex-direction:row;-webkit-flex-direction:row}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-default{color:#212324;background-color:#d7d8d8}.list-group-item-default.list-group-item-action:hover,.list-group-item-default.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-default.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.list-group-item-primary{color:#174d88;background-color:#d4e6f9}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#174d88;background-color:#bfcfe0}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#174d88;border-color:#174d88}.list-group-item-secondary{color:#212324;background-color:#d7d8d8}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.list-group-item-success{color:#266d0e;background-color:#d9f0d1}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#266d0e;background-color:#c3d8bc}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#266d0e;border-color:#266d0e}.list-group-item-info{color:#5c3270;background-color:#ebddf1}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#5c3270;background-color:#d4c7d9}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#5c3270;border-color:#5c3270}.list-group-item-warning{color:#99460e;background-color:#ffe3d1}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#99460e;background-color:#e6ccbc}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#99460e;border-color:#99460e}.list-group-item-danger{color:#902;background-color:#ffccd7}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#902;background-color:#e6b8c2}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#902;border-color:#902}.list-group-item-light{color:#959596;background-color:#fefefe}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#959596;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#959596;border-color:#959596}.list-group-item-dark{color:#212324;background-color:#d7d8d8}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#212324;background-color:#c2c2c2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#212324;border-color:#212324}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:rgba(0,0,0,0) url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(39,128,227,.25);opacity:1}.btn-close:disabled,.btn-close.disabled{pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:0.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:max-content;width:-webkit-max-content;width:-moz-max-content;width:-ms-max-content;width:-o-max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05)}.toast-header .btn-close{margin-right:-0.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0, -50px)}@media(prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;display:-webkit-flex;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6}.modal-header .btn-close{padding:.5rem .5rem;margin:-0.5rem -0.5rem -0.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;-webkit-flex:1 1 auto;padding:1rem}.modal-footer{display:flex;display:-webkit-flex;flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-shrink:0;-webkit-flex-shrink:0;align-items:center;-webkit-align-items:center;justify-content:flex-end;-webkit-justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6}.modal-footer>*{margin:.25rem}@media(min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media(min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media(min-width: 1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0}.modal-fullscreen .modal-body{overflow-y:auto}@media(max-width: 575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media(max-width: 767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media(max-width: 991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media(max-width: 1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media(max-width: 1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[data-popper-placement^=top]{padding:.4rem 0}.bs-tooltip-top .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow{bottom:0}.bs-tooltip-top .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-end,.bs-tooltip-auto[data-popper-placement^=right]{padding:0 .4rem}.bs-tooltip-end .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-end .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[data-popper-placement^=bottom]{padding:.4rem 0}.bs-tooltip-bottom .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow{top:0}.bs-tooltip-bottom .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-start,.bs-tooltip-auto[data-popper-placement^=left]{padding:0 .4rem}.bs-tooltip-start .tooltip-arrow,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-start .tooltip-arrow::before,.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000}.popover{position:absolute;top:0;left:0 /* rtl:ignore */;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2)}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::before,.popover .popover-arrow::after{position:absolute;display:block;content:"";border-color:rgba(0,0,0,0);border-style:solid}.bs-popover-top>.popover-arrow,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-top>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-end>.popover-arrow,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow{left:calc(-0.5rem - 1px);width:.5rem;height:1rem}.bs-popover-end>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-end>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom>.popover-arrow,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-bottom>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-start>.popover-arrow,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow{right:calc(-0.5rem - 1px);width:.5rem;height:1rem}.bs-popover-start>.popover-arrow::before,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-start>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#373a3c}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y;-webkit-touch-action:pan-y;-moz-touch-action:pan-y;-ms-touch-action:pan-y;-o-touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;-webkit-backface-visibility:hidden;-moz-backface-visibility:hidden;-ms-backface-visibility:hidden;-o-backface-visibility:hidden;transition:transform .6s ease-in-out}@media(prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-start),.active.carousel-item-end{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-end),.active.carousel-item-start{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end{z-index:1;opacity:1}.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{z-index:0;opacity:0;transition:opacity 0s .6s}@media(prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-start,.carousel-fade .active.carousel-item-end{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:center;-webkit-justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:none;border:0;opacity:.5;transition:opacity .15s ease}@media(prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;display:-webkit-flex;justify-content:center;-webkit-justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;-webkit-flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid rgba(0,0,0,0);border-bottom:10px solid rgba(0,0,0,0);opacity:.5;transition:opacity .6s ease}@media(prefers-reduced-motion: reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-prev-icon,.carousel-dark .carousel-control-next-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@keyframes spinner-border{to{transform:rotate(360deg) /* rtl:ignore */}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-0.125em;border:.25em solid currentColor;border-right-color:rgba(0,0,0,0);border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-0.125em;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media(prefers-reduced-motion: reduce){.spinner-border,.spinner-grow{animation-duration:1.5s;-webkit-animation-duration:1.5s;-moz-animation-duration:1.5s;-ms-animation-duration:1.5s;-o-animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;display:-webkit-flex;flex-direction:column;-webkit-flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media(prefers-reduced-motion: reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;display:-webkit-flex;align-items:center;-webkit-align-items:center;justify-content:space-between;-webkit-justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-0.5rem;margin-right:-0.5rem;margin-bottom:-0.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;-webkit-flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);-webkit-mask-image:linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%);mask-size:200% 100%;-webkit-mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{mask-position:-200% 0%;-webkit-mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-default{color:#373a3c}.link-default:hover,.link-default:focus{color:#2c2e30}.link-primary{color:#2780e3}.link-primary:hover,.link-primary:focus{color:#1f66b6}.link-secondary{color:#373a3c}.link-secondary:hover,.link-secondary:focus{color:#2c2e30}.link-success{color:#3fb618}.link-success:hover,.link-success:focus{color:#329213}.link-info{color:#9954bb}.link-info:hover,.link-info:focus{color:#7a4396}.link-warning{color:#ff7518}.link-warning:hover,.link-warning:focus{color:#cc5e13}.link-danger{color:#ff0039}.link-danger:hover,.link-danger:focus{color:#cc002e}.link-light{color:#f8f9fa}.link-light:hover,.link-light:focus{color:#f9fafb}.link-dark{color:#373a3c}.link-dark:hover,.link-dark:focus{color:#2c2e30}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio: 100%}.ratio-4x3{--bs-aspect-ratio: 75%}.ratio-16x9{--bs-aspect-ratio: 56.25%}.ratio-21x9{--bs-aspect-ratio: 42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}@media(min-width: 576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}}@media(min-width: 768px){.sticky-md-top{position:sticky;top:0;z-index:1020}}@media(min-width: 992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}}@media(min-width: 1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}}@media(min-width: 1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}}.hstack{display:flex;display:-webkit-flex;flex-direction:row;-webkit-flex-direction:row;align-items:center;-webkit-align-items:center;align-self:stretch;-webkit-align-self:stretch}.vstack{display:flex;display:-webkit-flex;flex:1 1 auto;-webkit-flex:1 1 auto;flex-direction:column;-webkit-flex-direction:column;align-self:stretch;-webkit-align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute !important;width:1px !important;height:1px !important;padding:0 !important;margin:-1px !important;overflow:hidden !important;clip:rect(0, 0, 0, 0) !important;white-space:nowrap !important;border:0 !important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;-webkit-align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.float-start{float:left !important}.float-end{float:right !important}.float-none{float:none !important}.opacity-0{opacity:0 !important}.opacity-25{opacity:.25 !important}.opacity-50{opacity:.5 !important}.opacity-75{opacity:.75 !important}.opacity-100{opacity:1 !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.overflow-visible{overflow:visible !important}.overflow-scroll{overflow:scroll !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-grid{display:grid !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}.d-none{display:none !important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15) !important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175) !important}.shadow-none{box-shadow:none !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.top-0{top:0 !important}.top-50{top:50% !important}.top-100{top:100% !important}.bottom-0{bottom:0 !important}.bottom-50{bottom:50% !important}.bottom-100{bottom:100% !important}.start-0{left:0 !important}.start-50{left:50% !important}.start-100{left:100% !important}.end-0{right:0 !important}.end-50{right:50% !important}.end-100{right:100% !important}.translate-middle{transform:translate(-50%, -50%) !important}.translate-middle-x{transform:translateX(-50%) !important}.translate-middle-y{transform:translateY(-50%) !important}.border{border:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-top-0{border-top:0 !important}.border-end{border-right:1px solid #dee2e6 !important}.border-end-0{border-right:0 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-bottom-0{border-bottom:0 !important}.border-start{border-left:1px solid #dee2e6 !important}.border-start-0{border-left:0 !important}.border-default{border-color:#373a3c !important}.border-primary{border-color:#2780e3 !important}.border-secondary{border-color:#373a3c !important}.border-success{border-color:#3fb618 !important}.border-info{border-color:#9954bb !important}.border-warning{border-color:#ff7518 !important}.border-danger{border-color:#ff0039 !important}.border-light{border-color:#f8f9fa !important}.border-dark{border-color:#373a3c !important}.border-white{border-color:#fff !important}.border-1{border-width:1px !important}.border-2{border-width:2px !important}.border-3{border-width:3px !important}.border-4{border-width:4px !important}.border-5{border-width:5px !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.mw-100{max-width:100% !important}.vw-100{width:100vw !important}.min-vw-100{min-width:100vw !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mh-100{max-height:100% !important}.vh-100{height:100vh !important}.min-vh-100{min-height:100vh !important}.flex-fill{flex:1 1 auto !important}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-0{gap:0 !important}.gap-1{gap:.25rem !important}.gap-2{gap:.5rem !important}.gap-3{gap:1rem !important}.gap-4{gap:1.5rem !important}.gap-5{gap:3rem !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.justify-content-evenly{justify-content:space-evenly !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}.order-first{order:-1 !important}.order-0{order:0 !important}.order-1{order:1 !important}.order-2{order:2 !important}.order-3{order:3 !important}.order-4{order:4 !important}.order-5{order:5 !important}.order-last{order:6 !important}.m-0{margin:0 !important}.m-1{margin:.25rem !important}.m-2{margin:.5rem !important}.m-3{margin:1rem !important}.m-4{margin:1.5rem !important}.m-5{margin:3rem !important}.m-auto{margin:auto !important}.mx-0{margin-right:0 !important;margin-left:0 !important}.mx-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-3{margin-right:1rem !important;margin-left:1rem !important}.mx-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-5{margin-right:3rem !important;margin-left:3rem !important}.mx-auto{margin-right:auto !important;margin-left:auto !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-0{margin-top:0 !important}.mt-1{margin-top:.25rem !important}.mt-2{margin-top:.5rem !important}.mt-3{margin-top:1rem !important}.mt-4{margin-top:1.5rem !important}.mt-5{margin-top:3rem !important}.mt-auto{margin-top:auto !important}.me-0{margin-right:0 !important}.me-1{margin-right:.25rem !important}.me-2{margin-right:.5rem !important}.me-3{margin-right:1rem !important}.me-4{margin-right:1.5rem !important}.me-5{margin-right:3rem !important}.me-auto{margin-right:auto !important}.mb-0{margin-bottom:0 !important}.mb-1{margin-bottom:.25rem !important}.mb-2{margin-bottom:.5rem !important}.mb-3{margin-bottom:1rem !important}.mb-4{margin-bottom:1.5rem !important}.mb-5{margin-bottom:3rem !important}.mb-auto{margin-bottom:auto !important}.ms-0{margin-left:0 !important}.ms-1{margin-left:.25rem !important}.ms-2{margin-left:.5rem !important}.ms-3{margin-left:1rem !important}.ms-4{margin-left:1.5rem !important}.ms-5{margin-left:3rem !important}.ms-auto{margin-left:auto !important}.p-0{padding:0 !important}.p-1{padding:.25rem !important}.p-2{padding:.5rem !important}.p-3{padding:1rem !important}.p-4{padding:1.5rem !important}.p-5{padding:3rem !important}.px-0{padding-right:0 !important;padding-left:0 !important}.px-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-3{padding-right:1rem !important;padding-left:1rem !important}.px-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-5{padding-right:3rem !important;padding-left:3rem !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-0{padding-top:0 !important}.pt-1{padding-top:.25rem !important}.pt-2{padding-top:.5rem !important}.pt-3{padding-top:1rem !important}.pt-4{padding-top:1.5rem !important}.pt-5{padding-top:3rem !important}.pe-0{padding-right:0 !important}.pe-1{padding-right:.25rem !important}.pe-2{padding-right:.5rem !important}.pe-3{padding-right:1rem !important}.pe-4{padding-right:1.5rem !important}.pe-5{padding-right:3rem !important}.pb-0{padding-bottom:0 !important}.pb-1{padding-bottom:.25rem !important}.pb-2{padding-bottom:.5rem !important}.pb-3{padding-bottom:1rem !important}.pb-4{padding-bottom:1.5rem !important}.pb-5{padding-bottom:3rem !important}.ps-0{padding-left:0 !important}.ps-1{padding-left:.25rem !important}.ps-2{padding-left:.5rem !important}.ps-3{padding-left:1rem !important}.ps-4{padding-left:1.5rem !important}.ps-5{padding-left:3rem !important}.font-monospace{font-family:var(--bs-font-monospace) !important}.fs-1{font-size:calc(1.325rem + 0.9vw) !important}.fs-2{font-size:calc(1.29rem + 0.48vw) !important}.fs-3{font-size:calc(1.27rem + 0.24vw) !important}.fs-4{font-size:1.25rem !important}.fs-5{font-size:1.1rem !important}.fs-6{font-size:1rem !important}.fst-italic{font-style:italic !important}.fst-normal{font-style:normal !important}.fw-light{font-weight:300 !important}.fw-lighter{font-weight:lighter !important}.fw-normal{font-weight:400 !important}.fw-bold{font-weight:700 !important}.fw-bolder{font-weight:bolder !important}.lh-1{line-height:1 !important}.lh-sm{line-height:1.25 !important}.lh-base{line-height:1.5 !important}.lh-lg{line-height:2 !important}.text-start{text-align:left !important}.text-end{text-align:right !important}.text-center{text-align:center !important}.text-decoration-none{text-decoration:none !important}.text-decoration-underline{text-decoration:underline !important}.text-decoration-line-through{text-decoration:line-through !important}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-break{word-wrap:break-word !important;word-break:break-word !important}.text-default{--bs-text-opacity: 1;color:rgba(var(--bs-default-rgb), var(--bs-text-opacity)) !important}.text-primary{--bs-text-opacity: 1;color:rgba(var(--bs-primary-rgb), var(--bs-text-opacity)) !important}.text-secondary{--bs-text-opacity: 1;color:rgba(var(--bs-secondary-rgb), var(--bs-text-opacity)) !important}.text-success{--bs-text-opacity: 1;color:rgba(var(--bs-success-rgb), var(--bs-text-opacity)) !important}.text-info{--bs-text-opacity: 1;color:rgba(var(--bs-info-rgb), var(--bs-text-opacity)) !important}.text-warning{--bs-text-opacity: 1;color:rgba(var(--bs-warning-rgb), var(--bs-text-opacity)) !important}.text-danger{--bs-text-opacity: 1;color:rgba(var(--bs-danger-rgb), var(--bs-text-opacity)) !important}.text-light{--bs-text-opacity: 1;color:rgba(var(--bs-light-rgb), var(--bs-text-opacity)) !important}.text-dark{--bs-text-opacity: 1;color:rgba(var(--bs-dark-rgb), var(--bs-text-opacity)) !important}.text-black{--bs-text-opacity: 1;color:rgba(var(--bs-black-rgb), var(--bs-text-opacity)) !important}.text-white{--bs-text-opacity: 1;color:rgba(var(--bs-white-rgb), var(--bs-text-opacity)) !important}.text-body{--bs-text-opacity: 1;color:rgba(var(--bs-body-color-rgb), var(--bs-text-opacity)) !important}.text-muted{--bs-text-opacity: 1;color:#6c757d !important}.text-black-50{--bs-text-opacity: 1;color:rgba(0,0,0,.5) !important}.text-white-50{--bs-text-opacity: 1;color:rgba(255,255,255,.5) !important}.text-reset{--bs-text-opacity: 1;color:inherit !important}.text-opacity-25{--bs-text-opacity: 0.25}.text-opacity-50{--bs-text-opacity: 0.5}.text-opacity-75{--bs-text-opacity: 0.75}.text-opacity-100{--bs-text-opacity: 1}.bg-default{--bs-bg-opacity: 1;background-color:rgba(var(--bs-default-rgb), var(--bs-bg-opacity)) !important}.bg-primary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important}.bg-secondary{--bs-bg-opacity: 1;background-color:rgba(var(--bs-secondary-rgb), var(--bs-bg-opacity)) !important}.bg-success{--bs-bg-opacity: 1;background-color:rgba(var(--bs-success-rgb), var(--bs-bg-opacity)) !important}.bg-info{--bs-bg-opacity: 1;background-color:rgba(var(--bs-info-rgb), var(--bs-bg-opacity)) !important}.bg-warning{--bs-bg-opacity: 1;background-color:rgba(var(--bs-warning-rgb), var(--bs-bg-opacity)) !important}.bg-danger{--bs-bg-opacity: 1;background-color:rgba(var(--bs-danger-rgb), var(--bs-bg-opacity)) !important}.bg-light{--bs-bg-opacity: 1;background-color:rgba(var(--bs-light-rgb), var(--bs-bg-opacity)) !important}.bg-dark{--bs-bg-opacity: 1;background-color:rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important}.bg-black{--bs-bg-opacity: 1;background-color:rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important}.bg-white{--bs-bg-opacity: 1;background-color:rgba(var(--bs-white-rgb), var(--bs-bg-opacity)) !important}.bg-body{--bs-bg-opacity: 1;background-color:rgba(var(--bs-body-bg-rgb), var(--bs-bg-opacity)) !important}.bg-transparent{--bs-bg-opacity: 1;background-color:rgba(0,0,0,0) !important}.bg-opacity-10{--bs-bg-opacity: 0.1}.bg-opacity-25{--bs-bg-opacity: 0.25}.bg-opacity-50{--bs-bg-opacity: 0.5}.bg-opacity-75{--bs-bg-opacity: 0.75}.bg-opacity-100{--bs-bg-opacity: 1}.bg-gradient{background-image:var(--bs-gradient) !important}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.pe-none{pointer-events:none !important}.pe-auto{pointer-events:auto !important}.rounded{border-radius:.25rem !important}.rounded-0{border-radius:0 !important}.rounded-1{border-radius:.2em !important}.rounded-2{border-radius:.25rem !important}.rounded-3{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-end{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-start{border-bottom-left-radius:.25rem !important;border-top-left-radius:.25rem !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media(min-width: 576px){.float-sm-start{float:left !important}.float-sm-end{float:right !important}.float-sm-none{float:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-grid{display:grid !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}.d-sm-none{display:none !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-sm-0{gap:0 !important}.gap-sm-1{gap:.25rem !important}.gap-sm-2{gap:.5rem !important}.gap-sm-3{gap:1rem !important}.gap-sm-4{gap:1.5rem !important}.gap-sm-5{gap:3rem !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.justify-content-sm-evenly{justify-content:space-evenly !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}.order-sm-first{order:-1 !important}.order-sm-0{order:0 !important}.order-sm-1{order:1 !important}.order-sm-2{order:2 !important}.order-sm-3{order:3 !important}.order-sm-4{order:4 !important}.order-sm-5{order:5 !important}.order-sm-last{order:6 !important}.m-sm-0{margin:0 !important}.m-sm-1{margin:.25rem !important}.m-sm-2{margin:.5rem !important}.m-sm-3{margin:1rem !important}.m-sm-4{margin:1.5rem !important}.m-sm-5{margin:3rem !important}.m-sm-auto{margin:auto !important}.mx-sm-0{margin-right:0 !important;margin-left:0 !important}.mx-sm-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-sm-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-sm-3{margin-right:1rem !important;margin-left:1rem !important}.mx-sm-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-sm-5{margin-right:3rem !important;margin-left:3rem !important}.mx-sm-auto{margin-right:auto !important;margin-left:auto !important}.my-sm-0{margin-top:0 !important;margin-bottom:0 !important}.my-sm-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-sm-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-sm-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-sm-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-sm-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-sm-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-sm-0{margin-top:0 !important}.mt-sm-1{margin-top:.25rem !important}.mt-sm-2{margin-top:.5rem !important}.mt-sm-3{margin-top:1rem !important}.mt-sm-4{margin-top:1.5rem !important}.mt-sm-5{margin-top:3rem !important}.mt-sm-auto{margin-top:auto !important}.me-sm-0{margin-right:0 !important}.me-sm-1{margin-right:.25rem !important}.me-sm-2{margin-right:.5rem !important}.me-sm-3{margin-right:1rem !important}.me-sm-4{margin-right:1.5rem !important}.me-sm-5{margin-right:3rem !important}.me-sm-auto{margin-right:auto !important}.mb-sm-0{margin-bottom:0 !important}.mb-sm-1{margin-bottom:.25rem !important}.mb-sm-2{margin-bottom:.5rem !important}.mb-sm-3{margin-bottom:1rem !important}.mb-sm-4{margin-bottom:1.5rem !important}.mb-sm-5{margin-bottom:3rem !important}.mb-sm-auto{margin-bottom:auto !important}.ms-sm-0{margin-left:0 !important}.ms-sm-1{margin-left:.25rem !important}.ms-sm-2{margin-left:.5rem !important}.ms-sm-3{margin-left:1rem !important}.ms-sm-4{margin-left:1.5rem !important}.ms-sm-5{margin-left:3rem !important}.ms-sm-auto{margin-left:auto !important}.p-sm-0{padding:0 !important}.p-sm-1{padding:.25rem !important}.p-sm-2{padding:.5rem !important}.p-sm-3{padding:1rem !important}.p-sm-4{padding:1.5rem !important}.p-sm-5{padding:3rem !important}.px-sm-0{padding-right:0 !important;padding-left:0 !important}.px-sm-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-sm-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-sm-3{padding-right:1rem !important;padding-left:1rem !important}.px-sm-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-sm-5{padding-right:3rem !important;padding-left:3rem !important}.py-sm-0{padding-top:0 !important;padding-bottom:0 !important}.py-sm-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-sm-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-sm-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-sm-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-sm-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-sm-0{padding-top:0 !important}.pt-sm-1{padding-top:.25rem !important}.pt-sm-2{padding-top:.5rem !important}.pt-sm-3{padding-top:1rem !important}.pt-sm-4{padding-top:1.5rem !important}.pt-sm-5{padding-top:3rem !important}.pe-sm-0{padding-right:0 !important}.pe-sm-1{padding-right:.25rem !important}.pe-sm-2{padding-right:.5rem !important}.pe-sm-3{padding-right:1rem !important}.pe-sm-4{padding-right:1.5rem !important}.pe-sm-5{padding-right:3rem !important}.pb-sm-0{padding-bottom:0 !important}.pb-sm-1{padding-bottom:.25rem !important}.pb-sm-2{padding-bottom:.5rem !important}.pb-sm-3{padding-bottom:1rem !important}.pb-sm-4{padding-bottom:1.5rem !important}.pb-sm-5{padding-bottom:3rem !important}.ps-sm-0{padding-left:0 !important}.ps-sm-1{padding-left:.25rem !important}.ps-sm-2{padding-left:.5rem !important}.ps-sm-3{padding-left:1rem !important}.ps-sm-4{padding-left:1.5rem !important}.ps-sm-5{padding-left:3rem !important}.text-sm-start{text-align:left !important}.text-sm-end{text-align:right !important}.text-sm-center{text-align:center !important}}@media(min-width: 768px){.float-md-start{float:left !important}.float-md-end{float:right !important}.float-md-none{float:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-grid{display:grid !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}.d-md-none{display:none !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-md-0{gap:0 !important}.gap-md-1{gap:.25rem !important}.gap-md-2{gap:.5rem !important}.gap-md-3{gap:1rem !important}.gap-md-4{gap:1.5rem !important}.gap-md-5{gap:3rem !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.justify-content-md-evenly{justify-content:space-evenly !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}.order-md-first{order:-1 !important}.order-md-0{order:0 !important}.order-md-1{order:1 !important}.order-md-2{order:2 !important}.order-md-3{order:3 !important}.order-md-4{order:4 !important}.order-md-5{order:5 !important}.order-md-last{order:6 !important}.m-md-0{margin:0 !important}.m-md-1{margin:.25rem !important}.m-md-2{margin:.5rem !important}.m-md-3{margin:1rem !important}.m-md-4{margin:1.5rem !important}.m-md-5{margin:3rem !important}.m-md-auto{margin:auto !important}.mx-md-0{margin-right:0 !important;margin-left:0 !important}.mx-md-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-md-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-md-3{margin-right:1rem !important;margin-left:1rem !important}.mx-md-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-md-5{margin-right:3rem !important;margin-left:3rem !important}.mx-md-auto{margin-right:auto !important;margin-left:auto !important}.my-md-0{margin-top:0 !important;margin-bottom:0 !important}.my-md-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-md-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-md-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-md-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-md-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-md-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-md-0{margin-top:0 !important}.mt-md-1{margin-top:.25rem !important}.mt-md-2{margin-top:.5rem !important}.mt-md-3{margin-top:1rem !important}.mt-md-4{margin-top:1.5rem !important}.mt-md-5{margin-top:3rem !important}.mt-md-auto{margin-top:auto !important}.me-md-0{margin-right:0 !important}.me-md-1{margin-right:.25rem !important}.me-md-2{margin-right:.5rem !important}.me-md-3{margin-right:1rem !important}.me-md-4{margin-right:1.5rem !important}.me-md-5{margin-right:3rem !important}.me-md-auto{margin-right:auto !important}.mb-md-0{margin-bottom:0 !important}.mb-md-1{margin-bottom:.25rem !important}.mb-md-2{margin-bottom:.5rem !important}.mb-md-3{margin-bottom:1rem !important}.mb-md-4{margin-bottom:1.5rem !important}.mb-md-5{margin-bottom:3rem !important}.mb-md-auto{margin-bottom:auto !important}.ms-md-0{margin-left:0 !important}.ms-md-1{margin-left:.25rem !important}.ms-md-2{margin-left:.5rem !important}.ms-md-3{margin-left:1rem !important}.ms-md-4{margin-left:1.5rem !important}.ms-md-5{margin-left:3rem !important}.ms-md-auto{margin-left:auto !important}.p-md-0{padding:0 !important}.p-md-1{padding:.25rem !important}.p-md-2{padding:.5rem !important}.p-md-3{padding:1rem !important}.p-md-4{padding:1.5rem !important}.p-md-5{padding:3rem !important}.px-md-0{padding-right:0 !important;padding-left:0 !important}.px-md-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-md-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-md-3{padding-right:1rem !important;padding-left:1rem !important}.px-md-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-md-5{padding-right:3rem !important;padding-left:3rem !important}.py-md-0{padding-top:0 !important;padding-bottom:0 !important}.py-md-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-md-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-md-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-md-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-md-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-md-0{padding-top:0 !important}.pt-md-1{padding-top:.25rem !important}.pt-md-2{padding-top:.5rem !important}.pt-md-3{padding-top:1rem !important}.pt-md-4{padding-top:1.5rem !important}.pt-md-5{padding-top:3rem !important}.pe-md-0{padding-right:0 !important}.pe-md-1{padding-right:.25rem !important}.pe-md-2{padding-right:.5rem !important}.pe-md-3{padding-right:1rem !important}.pe-md-4{padding-right:1.5rem !important}.pe-md-5{padding-right:3rem !important}.pb-md-0{padding-bottom:0 !important}.pb-md-1{padding-bottom:.25rem !important}.pb-md-2{padding-bottom:.5rem !important}.pb-md-3{padding-bottom:1rem !important}.pb-md-4{padding-bottom:1.5rem !important}.pb-md-5{padding-bottom:3rem !important}.ps-md-0{padding-left:0 !important}.ps-md-1{padding-left:.25rem !important}.ps-md-2{padding-left:.5rem !important}.ps-md-3{padding-left:1rem !important}.ps-md-4{padding-left:1.5rem !important}.ps-md-5{padding-left:3rem !important}.text-md-start{text-align:left !important}.text-md-end{text-align:right !important}.text-md-center{text-align:center !important}}@media(min-width: 992px){.float-lg-start{float:left !important}.float-lg-end{float:right !important}.float-lg-none{float:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-grid{display:grid !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}.d-lg-none{display:none !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-lg-0{gap:0 !important}.gap-lg-1{gap:.25rem !important}.gap-lg-2{gap:.5rem !important}.gap-lg-3{gap:1rem !important}.gap-lg-4{gap:1.5rem !important}.gap-lg-5{gap:3rem !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.justify-content-lg-evenly{justify-content:space-evenly !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}.order-lg-first{order:-1 !important}.order-lg-0{order:0 !important}.order-lg-1{order:1 !important}.order-lg-2{order:2 !important}.order-lg-3{order:3 !important}.order-lg-4{order:4 !important}.order-lg-5{order:5 !important}.order-lg-last{order:6 !important}.m-lg-0{margin:0 !important}.m-lg-1{margin:.25rem !important}.m-lg-2{margin:.5rem !important}.m-lg-3{margin:1rem !important}.m-lg-4{margin:1.5rem !important}.m-lg-5{margin:3rem !important}.m-lg-auto{margin:auto !important}.mx-lg-0{margin-right:0 !important;margin-left:0 !important}.mx-lg-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-lg-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-lg-3{margin-right:1rem !important;margin-left:1rem !important}.mx-lg-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-lg-5{margin-right:3rem !important;margin-left:3rem !important}.mx-lg-auto{margin-right:auto !important;margin-left:auto !important}.my-lg-0{margin-top:0 !important;margin-bottom:0 !important}.my-lg-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-lg-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-lg-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-lg-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-lg-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-lg-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-lg-0{margin-top:0 !important}.mt-lg-1{margin-top:.25rem !important}.mt-lg-2{margin-top:.5rem !important}.mt-lg-3{margin-top:1rem !important}.mt-lg-4{margin-top:1.5rem !important}.mt-lg-5{margin-top:3rem !important}.mt-lg-auto{margin-top:auto !important}.me-lg-0{margin-right:0 !important}.me-lg-1{margin-right:.25rem !important}.me-lg-2{margin-right:.5rem !important}.me-lg-3{margin-right:1rem !important}.me-lg-4{margin-right:1.5rem !important}.me-lg-5{margin-right:3rem !important}.me-lg-auto{margin-right:auto !important}.mb-lg-0{margin-bottom:0 !important}.mb-lg-1{margin-bottom:.25rem !important}.mb-lg-2{margin-bottom:.5rem !important}.mb-lg-3{margin-bottom:1rem !important}.mb-lg-4{margin-bottom:1.5rem !important}.mb-lg-5{margin-bottom:3rem !important}.mb-lg-auto{margin-bottom:auto !important}.ms-lg-0{margin-left:0 !important}.ms-lg-1{margin-left:.25rem !important}.ms-lg-2{margin-left:.5rem !important}.ms-lg-3{margin-left:1rem !important}.ms-lg-4{margin-left:1.5rem !important}.ms-lg-5{margin-left:3rem !important}.ms-lg-auto{margin-left:auto !important}.p-lg-0{padding:0 !important}.p-lg-1{padding:.25rem !important}.p-lg-2{padding:.5rem !important}.p-lg-3{padding:1rem !important}.p-lg-4{padding:1.5rem !important}.p-lg-5{padding:3rem !important}.px-lg-0{padding-right:0 !important;padding-left:0 !important}.px-lg-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-lg-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-lg-3{padding-right:1rem !important;padding-left:1rem !important}.px-lg-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-lg-5{padding-right:3rem !important;padding-left:3rem !important}.py-lg-0{padding-top:0 !important;padding-bottom:0 !important}.py-lg-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-lg-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-lg-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-lg-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-lg-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-lg-0{padding-top:0 !important}.pt-lg-1{padding-top:.25rem !important}.pt-lg-2{padding-top:.5rem !important}.pt-lg-3{padding-top:1rem !important}.pt-lg-4{padding-top:1.5rem !important}.pt-lg-5{padding-top:3rem !important}.pe-lg-0{padding-right:0 !important}.pe-lg-1{padding-right:.25rem !important}.pe-lg-2{padding-right:.5rem !important}.pe-lg-3{padding-right:1rem !important}.pe-lg-4{padding-right:1.5rem !important}.pe-lg-5{padding-right:3rem !important}.pb-lg-0{padding-bottom:0 !important}.pb-lg-1{padding-bottom:.25rem !important}.pb-lg-2{padding-bottom:.5rem !important}.pb-lg-3{padding-bottom:1rem !important}.pb-lg-4{padding-bottom:1.5rem !important}.pb-lg-5{padding-bottom:3rem !important}.ps-lg-0{padding-left:0 !important}.ps-lg-1{padding-left:.25rem !important}.ps-lg-2{padding-left:.5rem !important}.ps-lg-3{padding-left:1rem !important}.ps-lg-4{padding-left:1.5rem !important}.ps-lg-5{padding-left:3rem !important}.text-lg-start{text-align:left !important}.text-lg-end{text-align:right !important}.text-lg-center{text-align:center !important}}@media(min-width: 1200px){.float-xl-start{float:left !important}.float-xl-end{float:right !important}.float-xl-none{float:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-grid{display:grid !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}.d-xl-none{display:none !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-xl-0{gap:0 !important}.gap-xl-1{gap:.25rem !important}.gap-xl-2{gap:.5rem !important}.gap-xl-3{gap:1rem !important}.gap-xl-4{gap:1.5rem !important}.gap-xl-5{gap:3rem !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.justify-content-xl-evenly{justify-content:space-evenly !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}.order-xl-first{order:-1 !important}.order-xl-0{order:0 !important}.order-xl-1{order:1 !important}.order-xl-2{order:2 !important}.order-xl-3{order:3 !important}.order-xl-4{order:4 !important}.order-xl-5{order:5 !important}.order-xl-last{order:6 !important}.m-xl-0{margin:0 !important}.m-xl-1{margin:.25rem !important}.m-xl-2{margin:.5rem !important}.m-xl-3{margin:1rem !important}.m-xl-4{margin:1.5rem !important}.m-xl-5{margin:3rem !important}.m-xl-auto{margin:auto !important}.mx-xl-0{margin-right:0 !important;margin-left:0 !important}.mx-xl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xl-auto{margin-right:auto !important;margin-left:auto !important}.my-xl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xl-0{margin-top:0 !important}.mt-xl-1{margin-top:.25rem !important}.mt-xl-2{margin-top:.5rem !important}.mt-xl-3{margin-top:1rem !important}.mt-xl-4{margin-top:1.5rem !important}.mt-xl-5{margin-top:3rem !important}.mt-xl-auto{margin-top:auto !important}.me-xl-0{margin-right:0 !important}.me-xl-1{margin-right:.25rem !important}.me-xl-2{margin-right:.5rem !important}.me-xl-3{margin-right:1rem !important}.me-xl-4{margin-right:1.5rem !important}.me-xl-5{margin-right:3rem !important}.me-xl-auto{margin-right:auto !important}.mb-xl-0{margin-bottom:0 !important}.mb-xl-1{margin-bottom:.25rem !important}.mb-xl-2{margin-bottom:.5rem !important}.mb-xl-3{margin-bottom:1rem !important}.mb-xl-4{margin-bottom:1.5rem !important}.mb-xl-5{margin-bottom:3rem !important}.mb-xl-auto{margin-bottom:auto !important}.ms-xl-0{margin-left:0 !important}.ms-xl-1{margin-left:.25rem !important}.ms-xl-2{margin-left:.5rem !important}.ms-xl-3{margin-left:1rem !important}.ms-xl-4{margin-left:1.5rem !important}.ms-xl-5{margin-left:3rem !important}.ms-xl-auto{margin-left:auto !important}.p-xl-0{padding:0 !important}.p-xl-1{padding:.25rem !important}.p-xl-2{padding:.5rem !important}.p-xl-3{padding:1rem !important}.p-xl-4{padding:1.5rem !important}.p-xl-5{padding:3rem !important}.px-xl-0{padding-right:0 !important;padding-left:0 !important}.px-xl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xl-0{padding-top:0 !important}.pt-xl-1{padding-top:.25rem !important}.pt-xl-2{padding-top:.5rem !important}.pt-xl-3{padding-top:1rem !important}.pt-xl-4{padding-top:1.5rem !important}.pt-xl-5{padding-top:3rem !important}.pe-xl-0{padding-right:0 !important}.pe-xl-1{padding-right:.25rem !important}.pe-xl-2{padding-right:.5rem !important}.pe-xl-3{padding-right:1rem !important}.pe-xl-4{padding-right:1.5rem !important}.pe-xl-5{padding-right:3rem !important}.pb-xl-0{padding-bottom:0 !important}.pb-xl-1{padding-bottom:.25rem !important}.pb-xl-2{padding-bottom:.5rem !important}.pb-xl-3{padding-bottom:1rem !important}.pb-xl-4{padding-bottom:1.5rem !important}.pb-xl-5{padding-bottom:3rem !important}.ps-xl-0{padding-left:0 !important}.ps-xl-1{padding-left:.25rem !important}.ps-xl-2{padding-left:.5rem !important}.ps-xl-3{padding-left:1rem !important}.ps-xl-4{padding-left:1.5rem !important}.ps-xl-5{padding-left:3rem !important}.text-xl-start{text-align:left !important}.text-xl-end{text-align:right !important}.text-xl-center{text-align:center !important}}@media(min-width: 1400px){.float-xxl-start{float:left !important}.float-xxl-end{float:right !important}.float-xxl-none{float:none !important}.d-xxl-inline{display:inline !important}.d-xxl-inline-block{display:inline-block !important}.d-xxl-block{display:block !important}.d-xxl-grid{display:grid !important}.d-xxl-table{display:table !important}.d-xxl-table-row{display:table-row !important}.d-xxl-table-cell{display:table-cell !important}.d-xxl-flex{display:flex !important}.d-xxl-inline-flex{display:inline-flex !important}.d-xxl-none{display:none !important}.flex-xxl-fill{flex:1 1 auto !important}.flex-xxl-row{flex-direction:row !important}.flex-xxl-column{flex-direction:column !important}.flex-xxl-row-reverse{flex-direction:row-reverse !important}.flex-xxl-column-reverse{flex-direction:column-reverse !important}.flex-xxl-grow-0{flex-grow:0 !important}.flex-xxl-grow-1{flex-grow:1 !important}.flex-xxl-shrink-0{flex-shrink:0 !important}.flex-xxl-shrink-1{flex-shrink:1 !important}.flex-xxl-wrap{flex-wrap:wrap !important}.flex-xxl-nowrap{flex-wrap:nowrap !important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse !important}.gap-xxl-0{gap:0 !important}.gap-xxl-1{gap:.25rem !important}.gap-xxl-2{gap:.5rem !important}.gap-xxl-3{gap:1rem !important}.gap-xxl-4{gap:1.5rem !important}.gap-xxl-5{gap:3rem !important}.justify-content-xxl-start{justify-content:flex-start !important}.justify-content-xxl-end{justify-content:flex-end !important}.justify-content-xxl-center{justify-content:center !important}.justify-content-xxl-between{justify-content:space-between !important}.justify-content-xxl-around{justify-content:space-around !important}.justify-content-xxl-evenly{justify-content:space-evenly !important}.align-items-xxl-start{align-items:flex-start !important}.align-items-xxl-end{align-items:flex-end !important}.align-items-xxl-center{align-items:center !important}.align-items-xxl-baseline{align-items:baseline !important}.align-items-xxl-stretch{align-items:stretch !important}.align-content-xxl-start{align-content:flex-start !important}.align-content-xxl-end{align-content:flex-end !important}.align-content-xxl-center{align-content:center !important}.align-content-xxl-between{align-content:space-between !important}.align-content-xxl-around{align-content:space-around !important}.align-content-xxl-stretch{align-content:stretch !important}.align-self-xxl-auto{align-self:auto !important}.align-self-xxl-start{align-self:flex-start !important}.align-self-xxl-end{align-self:flex-end !important}.align-self-xxl-center{align-self:center !important}.align-self-xxl-baseline{align-self:baseline !important}.align-self-xxl-stretch{align-self:stretch !important}.order-xxl-first{order:-1 !important}.order-xxl-0{order:0 !important}.order-xxl-1{order:1 !important}.order-xxl-2{order:2 !important}.order-xxl-3{order:3 !important}.order-xxl-4{order:4 !important}.order-xxl-5{order:5 !important}.order-xxl-last{order:6 !important}.m-xxl-0{margin:0 !important}.m-xxl-1{margin:.25rem !important}.m-xxl-2{margin:.5rem !important}.m-xxl-3{margin:1rem !important}.m-xxl-4{margin:1.5rem !important}.m-xxl-5{margin:3rem !important}.m-xxl-auto{margin:auto !important}.mx-xxl-0{margin-right:0 !important;margin-left:0 !important}.mx-xxl-1{margin-right:.25rem !important;margin-left:.25rem !important}.mx-xxl-2{margin-right:.5rem !important;margin-left:.5rem !important}.mx-xxl-3{margin-right:1rem !important;margin-left:1rem !important}.mx-xxl-4{margin-right:1.5rem !important;margin-left:1.5rem !important}.mx-xxl-5{margin-right:3rem !important;margin-left:3rem !important}.mx-xxl-auto{margin-right:auto !important;margin-left:auto !important}.my-xxl-0{margin-top:0 !important;margin-bottom:0 !important}.my-xxl-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.my-xxl-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.my-xxl-3{margin-top:1rem !important;margin-bottom:1rem !important}.my-xxl-4{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.my-xxl-5{margin-top:3rem !important;margin-bottom:3rem !important}.my-xxl-auto{margin-top:auto !important;margin-bottom:auto !important}.mt-xxl-0{margin-top:0 !important}.mt-xxl-1{margin-top:.25rem !important}.mt-xxl-2{margin-top:.5rem !important}.mt-xxl-3{margin-top:1rem !important}.mt-xxl-4{margin-top:1.5rem !important}.mt-xxl-5{margin-top:3rem !important}.mt-xxl-auto{margin-top:auto !important}.me-xxl-0{margin-right:0 !important}.me-xxl-1{margin-right:.25rem !important}.me-xxl-2{margin-right:.5rem !important}.me-xxl-3{margin-right:1rem !important}.me-xxl-4{margin-right:1.5rem !important}.me-xxl-5{margin-right:3rem !important}.me-xxl-auto{margin-right:auto !important}.mb-xxl-0{margin-bottom:0 !important}.mb-xxl-1{margin-bottom:.25rem !important}.mb-xxl-2{margin-bottom:.5rem !important}.mb-xxl-3{margin-bottom:1rem !important}.mb-xxl-4{margin-bottom:1.5rem !important}.mb-xxl-5{margin-bottom:3rem !important}.mb-xxl-auto{margin-bottom:auto !important}.ms-xxl-0{margin-left:0 !important}.ms-xxl-1{margin-left:.25rem !important}.ms-xxl-2{margin-left:.5rem !important}.ms-xxl-3{margin-left:1rem !important}.ms-xxl-4{margin-left:1.5rem !important}.ms-xxl-5{margin-left:3rem !important}.ms-xxl-auto{margin-left:auto !important}.p-xxl-0{padding:0 !important}.p-xxl-1{padding:.25rem !important}.p-xxl-2{padding:.5rem !important}.p-xxl-3{padding:1rem !important}.p-xxl-4{padding:1.5rem !important}.p-xxl-5{padding:3rem !important}.px-xxl-0{padding-right:0 !important;padding-left:0 !important}.px-xxl-1{padding-right:.25rem !important;padding-left:.25rem !important}.px-xxl-2{padding-right:.5rem !important;padding-left:.5rem !important}.px-xxl-3{padding-right:1rem !important;padding-left:1rem !important}.px-xxl-4{padding-right:1.5rem !important;padding-left:1.5rem !important}.px-xxl-5{padding-right:3rem !important;padding-left:3rem !important}.py-xxl-0{padding-top:0 !important;padding-bottom:0 !important}.py-xxl-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.py-xxl-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.py-xxl-3{padding-top:1rem !important;padding-bottom:1rem !important}.py-xxl-4{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.py-xxl-5{padding-top:3rem !important;padding-bottom:3rem !important}.pt-xxl-0{padding-top:0 !important}.pt-xxl-1{padding-top:.25rem !important}.pt-xxl-2{padding-top:.5rem !important}.pt-xxl-3{padding-top:1rem !important}.pt-xxl-4{padding-top:1.5rem !important}.pt-xxl-5{padding-top:3rem !important}.pe-xxl-0{padding-right:0 !important}.pe-xxl-1{padding-right:.25rem !important}.pe-xxl-2{padding-right:.5rem !important}.pe-xxl-3{padding-right:1rem !important}.pe-xxl-4{padding-right:1.5rem !important}.pe-xxl-5{padding-right:3rem !important}.pb-xxl-0{padding-bottom:0 !important}.pb-xxl-1{padding-bottom:.25rem !important}.pb-xxl-2{padding-bottom:.5rem !important}.pb-xxl-3{padding-bottom:1rem !important}.pb-xxl-4{padding-bottom:1.5rem !important}.pb-xxl-5{padding-bottom:3rem !important}.ps-xxl-0{padding-left:0 !important}.ps-xxl-1{padding-left:.25rem !important}.ps-xxl-2{padding-left:.5rem !important}.ps-xxl-3{padding-left:1rem !important}.ps-xxl-4{padding-left:1.5rem !important}.ps-xxl-5{padding-left:3rem !important}.text-xxl-start{text-align:left !important}.text-xxl-end{text-align:right !important}.text-xxl-center{text-align:center !important}}.bg-default{color:#fff}.bg-primary{color:#fff}.bg-secondary{color:#fff}.bg-success{color:#fff}.bg-info{color:#fff}.bg-warning{color:#fff}.bg-danger{color:#fff}.bg-light{color:#000}.bg-dark{color:#fff}@media(min-width: 1200px){.fs-1{font-size:2rem !important}.fs-2{font-size:1.65rem !important}.fs-3{font-size:1.45rem !important}}@media print{.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-grid{display:grid !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}.d-print-none{display:none !important}}.sidebar-item .chapter-number{color:#373a3c}.quarto-container{min-height:calc(100vh - 132px)}footer.footer .nav-footer,#quarto-header>nav{padding-left:1em;padding-right:1em}nav[role=doc-toc]{padding-left:.5em}#quarto-content>*{padding-top:14px}@media(max-width: 991.98px){#quarto-content>*{padding-top:0}#quarto-content .subtitle{padding-top:14px}#quarto-content section:first-of-type h2:first-of-type,#quarto-content section:first-of-type .h2:first-of-type{margin-top:1rem}}.headroom-target,header.headroom{will-change:transform;transition:position 200ms linear;transition:all 200ms linear}header.headroom--pinned{transform:translateY(0%)}header.headroom--unpinned{transform:translateY(-100%)}.navbar-container{width:100%}.navbar-brand{overflow:hidden;text-overflow:ellipsis}.navbar-brand-container{max-width:calc(100% - 115px);min-width:0;display:flex;align-items:center}@media(min-width: 992px){.navbar-brand-container{margin-right:1em}}.navbar-brand.navbar-brand-logo{margin-right:4px;display:inline-flex}.navbar-toggler{flex-basis:content;flex-shrink:0}.navbar .navbar-toggler{order:-1;margin-right:.5em}.navbar-logo{max-height:24px;width:auto;padding-right:4px}nav .nav-item:not(.compact){padding-top:1px}nav .nav-link i,nav .dropdown-item i{padding-right:1px}.navbar-expand-lg .navbar-nav .nav-link{padding-left:.6rem;padding-right:.6rem}nav .nav-item.compact .nav-link{padding-left:.5rem;padding-right:.5rem;font-size:1.1rem}.navbar .quarto-navbar-tools div.dropdown{display:inline-block}.navbar .quarto-navbar-tools .quarto-navigation-tool{color:#545555}.navbar .quarto-navbar-tools .quarto-navigation-tool:hover{color:#1a5698}@media(max-width: 991.98px){.navbar .quarto-navbar-tools{margin-top:.25em;padding-top:.75em;display:block;color:solid #d4d4d4 1px;text-align:center;vertical-align:middle;margin-right:auto}}.navbar-nav .dropdown-menu{min-width:220px;font-size:.9rem}.navbar .navbar-nav .nav-link.dropdown-toggle::after{opacity:.75;vertical-align:.175em}.navbar ul.dropdown-menu{padding-top:0;padding-bottom:0}.navbar .dropdown-header{text-transform:uppercase;font-size:.8rem;padding:0 .5rem}.navbar .dropdown-item{padding:.4rem .5rem}.navbar .dropdown-item>i.bi{margin-left:.1rem;margin-right:.25em}.sidebar #quarto-search{margin-top:-1px}.sidebar #quarto-search svg.aa-SubmitIcon{width:16px;height:16px}.sidebar-navigation a{color:inherit}.sidebar-title{margin-top:.25rem;padding-bottom:.5rem;font-size:1.3rem;line-height:1.6rem;visibility:visible}.sidebar-title>a{font-size:inherit;text-decoration:none}.sidebar-title .sidebar-tools-main{margin-top:-6px}@media(max-width: 991.98px){#quarto-sidebar div.sidebar-header{padding-top:.2em}}.sidebar-header-stacked .sidebar-title{margin-top:.6rem}.sidebar-logo{max-width:90%;padding-bottom:.5rem}.sidebar-logo-link{text-decoration:none}.sidebar-navigation li a{text-decoration:none}.sidebar-navigation .quarto-navigation-tool{opacity:.7;font-size:.875rem}#quarto-sidebar>nav>.sidebar-tools-main{margin-left:14px}.sidebar-tools-main{display:inline-flex;margin-left:0px;order:2}.sidebar-tools-main:not(.tools-wide){vertical-align:middle}.sidebar-navigation .quarto-navigation-tool.dropdown-toggle::after{display:none}.sidebar.sidebar-navigation>*{padding-top:1em}.sidebar-item{margin-bottom:.2em}.sidebar-section{margin-top:.2em;padding-left:.5em;padding-bottom:.2em}.sidebar-item .sidebar-item-container{display:flex;justify-content:space-between}.sidebar-item-toggle:hover{cursor:pointer}.sidebar-item .sidebar-item-toggle .bi{font-size:.7rem;text-align:center}.sidebar-item .sidebar-item-toggle .bi-chevron-right::before{transition:transform 200ms ease}.sidebar-item .sidebar-item-toggle[aria-expanded=false] .bi-chevron-right::before{transform:none}.sidebar-item .sidebar-item-toggle[aria-expanded=true] .bi-chevron-right::before{transform:rotate(90deg)}.sidebar-navigation .sidebar-divider{margin-left:0;margin-right:0;margin-top:.5rem;margin-bottom:.5rem}@media(max-width: 991.98px){.quarto-secondary-nav{display:block}.quarto-secondary-nav button.quarto-search-button{padding-right:0em;padding-left:2em}.quarto-secondary-nav button.quarto-btn-toggle{margin-left:-0.75rem;margin-right:.15rem}.quarto-secondary-nav nav.quarto-page-breadcrumbs{display:flex;align-items:center;padding-right:1em;margin-left:-0.25em}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{text-decoration:none}.quarto-secondary-nav nav.quarto-page-breadcrumbs ol.breadcrumb{margin-bottom:0}}@media(min-width: 992px){.quarto-secondary-nav{display:none}}.quarto-secondary-nav .quarto-btn-toggle{color:#595959}.quarto-secondary-nav[aria-expanded=false] .quarto-btn-toggle .bi-chevron-right::before{transform:none}.quarto-secondary-nav[aria-expanded=true] .quarto-btn-toggle .bi-chevron-right::before{transform:rotate(90deg)}.quarto-secondary-nav .quarto-btn-toggle .bi-chevron-right::before{transition:transform 200ms ease}.quarto-secondary-nav{cursor:pointer}.quarto-secondary-nav-title{margin-top:.3em;color:#595959;padding-top:4px}.quarto-secondary-nav nav.quarto-page-breadcrumbs{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a{color:#595959}.quarto-secondary-nav nav.quarto-page-breadcrumbs a:hover{color:rgba(27,88,157,.8)}.quarto-secondary-nav nav.quarto-page-breadcrumbs .breadcrumb-item::before{color:#8c8c8c}div.sidebar-item-container{color:#595959}div.sidebar-item-container:hover,div.sidebar-item-container:focus{color:rgba(27,88,157,.8)}div.sidebar-item-container.disabled{color:rgba(89,89,89,.75)}div.sidebar-item-container .active,div.sidebar-item-container .show>.nav-link,div.sidebar-item-container .sidebar-link>code{color:#1b589d}div.sidebar.sidebar-navigation.rollup.quarto-sidebar-toggle-contents,nav.sidebar.sidebar-navigation:not(.rollup){background-color:#fff}@media(max-width: 991.98px){.sidebar-navigation .sidebar-item a,.nav-page .nav-page-text,.sidebar-navigation{font-size:1rem}.sidebar-navigation ul.sidebar-section.depth1 .sidebar-section-item{font-size:1.1rem}.sidebar-logo{display:none}.sidebar.sidebar-navigation{position:static;border-bottom:1px solid #dee2e6}.sidebar.sidebar-navigation.collapsing{position:fixed;z-index:1000}.sidebar.sidebar-navigation.show{position:fixed;z-index:1000}.sidebar.sidebar-navigation{min-height:100%}nav.quarto-secondary-nav{background-color:#fff;border-bottom:1px solid #dee2e6}.sidebar .sidebar-footer{visibility:visible;padding-top:1rem;position:inherit}.sidebar-tools-collapse{display:block}}#quarto-sidebar{transition:width .15s ease-in}#quarto-sidebar>*{padding-right:1em}@media(max-width: 991.98px){#quarto-sidebar .sidebar-menu-container{white-space:nowrap;min-width:225px}#quarto-sidebar.show{transition:width .15s ease-out}}@media(min-width: 992px){#quarto-sidebar{display:flex;flex-direction:column}.nav-page .nav-page-text,.sidebar-navigation .sidebar-section .sidebar-item{font-size:.875rem}.sidebar-navigation .sidebar-item{font-size:.925rem}.sidebar.sidebar-navigation{display:block;position:sticky}.sidebar-search{width:100%}.sidebar .sidebar-footer{visibility:visible}}@media(max-width: 991.98px){#quarto-sidebar-glass{position:fixed;top:0;bottom:0;left:0;right:0;background-color:rgba(255,255,255,0);transition:background-color .15s ease-in;z-index:-1}#quarto-sidebar-glass.collapsing{z-index:1000}#quarto-sidebar-glass.show{transition:background-color .15s ease-out;background-color:rgba(102,102,102,.4);z-index:1000}}.sidebar .sidebar-footer{padding:.5rem 1rem;align-self:flex-end;color:#6c757d;width:100%}.quarto-page-breadcrumbs .breadcrumb-item+.breadcrumb-item,.quarto-page-breadcrumbs .breadcrumb-item{padding-right:.33em;padding-left:0}.quarto-page-breadcrumbs .breadcrumb-item::before{padding-right:.33em}.quarto-sidebar-footer{font-size:.875em}.sidebar-section .bi-chevron-right{vertical-align:middle}.sidebar-section .bi-chevron-right::before{font-size:.9em}.notransition{-webkit-transition:none !important;-moz-transition:none !important;-o-transition:none !important;transition:none !important}.btn:focus:not(:focus-visible){box-shadow:none}.page-navigation{display:flex;justify-content:space-between}.nav-page{padding-bottom:.75em}.nav-page .bi{font-size:1.8rem;vertical-align:middle}.nav-page .nav-page-text{padding-left:.25em;padding-right:.25em}.nav-page a{color:#6c757d;text-decoration:none;display:flex;align-items:center}.nav-page a:hover{color:#1f66b6}.toc-actions{display:flex}.toc-actions p{margin-block-start:0;margin-block-end:0}.toc-actions a{text-decoration:none;color:inherit;font-weight:400}.toc-actions a:hover{color:#1f66b6}.toc-actions .action-links{margin-left:4px}.sidebar nav[role=doc-toc] .toc-actions .bi{margin-left:-4px;font-size:.7rem;color:#6c757d}.sidebar nav[role=doc-toc] .toc-actions .bi:before{padding-top:3px}#quarto-margin-sidebar .toc-actions .bi:before{margin-top:.3rem;font-size:.7rem;color:#6c757d;vertical-align:top}.sidebar nav[role=doc-toc] .toc-actions>div:first-of-type{margin-top:-3px}#quarto-margin-sidebar .toc-actions p,.sidebar nav[role=doc-toc] .toc-actions p{font-size:.875rem}.nav-footer .toc-actions{padding-bottom:.5em;padding-top:.5em}.nav-footer .toc-actions :first-child{margin-left:auto}.nav-footer .toc-actions :last-child{margin-right:auto}.nav-footer .toc-actions .action-links{display:flex}.nav-footer .toc-actions .action-links p{padding-right:1.5em}.nav-footer .toc-actions .action-links p:last-of-type{padding-right:0}.nav-footer{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:space-between;align-items:baseline;text-align:center;padding-top:.5rem;padding-bottom:.5rem;background-color:#fff}body.nav-fixed{padding-top:64px}.nav-footer-contents{color:#6c757d;margin-top:.25rem}.nav-footer{min-height:3.5em;color:#757575}.nav-footer a{color:#757575}.nav-footer .nav-footer-left{font-size:.825em}.nav-footer .nav-footer-center{font-size:.825em}.nav-footer .nav-footer-right{font-size:.825em}.nav-footer-left .footer-items,.nav-footer-center .footer-items,.nav-footer-right .footer-items{display:inline-flex;padding-top:.3em;padding-bottom:.3em;margin-bottom:0em}.nav-footer-left .footer-items .nav-link,.nav-footer-center .footer-items .nav-link,.nav-footer-right .footer-items .nav-link{padding-left:.6em;padding-right:.6em}.nav-footer-left{flex:1 1 0px;text-align:left}.nav-footer-right{flex:1 1 0px;text-align:right}.nav-footer-center{flex:1 1 0px;min-height:3em;text-align:center}.nav-footer-center .footer-items{justify-content:center}@media(max-width: 767.98px){.nav-footer-center{margin-top:3em}}.navbar .quarto-reader-toggle.reader .quarto-reader-toggle-btn{background-color:#545555;border-radius:3px}.quarto-reader-toggle.reader.quarto-navigation-tool .quarto-reader-toggle-btn{background-color:#595959;border-radius:3px}.quarto-reader-toggle .quarto-reader-toggle-btn{display:inline-flex;padding-left:.2em;padding-right:.2em;margin-left:-0.2em;margin-right:-0.2em;text-align:center}.navbar .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle:not(.reader) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-reader-toggle.reader .bi::before{background-image:url('data:image/svg+xml,')}#quarto-back-to-top{display:none;position:fixed;bottom:50px;background-color:#fff;border-radius:.25rem;box-shadow:0 .2rem .5rem #6c757d,0 0 .05rem #6c757d;color:#6c757d;text-decoration:none;font-size:.9em;text-align:center;left:50%;padding:.4rem .8rem;transform:translate(-50%, 0)}.aa-DetachedOverlay ul.aa-List,#quarto-search-results ul.aa-List{list-style:none;padding-left:0}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{background-color:#fff;position:absolute;z-index:2000}#quarto-search-results .aa-Panel{max-width:400px}#quarto-search input{font-size:.925rem}@media(min-width: 992px){.navbar #quarto-search{margin-left:.25rem;order:999}}@media(max-width: 991.98px){#quarto-sidebar .sidebar-search{display:none}}#quarto-sidebar .sidebar-search .aa-Autocomplete{width:100%}.navbar .aa-Autocomplete .aa-Form{width:180px}.navbar #quarto-search.type-overlay .aa-Autocomplete{width:40px}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form{background-color:inherit;border:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form:focus-within{box-shadow:none;outline:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper{display:none}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-InputWrapper:focus-within{display:inherit}.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-Label svg,.navbar #quarto-search.type-overlay .aa-Autocomplete .aa-Form .aa-LoadingIndicator svg{width:26px;height:26px;color:#545555;opacity:1}.navbar #quarto-search.type-overlay .aa-Autocomplete svg.aa-SubmitIcon{width:26px;height:26px;color:#545555;opacity:1}.aa-Autocomplete .aa-Form,.aa-DetachedFormContainer .aa-Form{align-items:center;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem;color:#373a3c;display:flex;line-height:1em;margin:0;position:relative;width:100%}.aa-Autocomplete .aa-Form:focus-within,.aa-DetachedFormContainer .aa-Form:focus-within{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix{align-items:center;display:flex;flex-shrink:0;order:1}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{cursor:initial;flex-shrink:0;padding:0;text-align:left}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-Label svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator svg{color:#373a3c;opacity:.5}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-SubmitButton{appearance:none;background:none;border:0;margin:0}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator{align-items:center;display:flex;justify-content:center}.aa-Autocomplete .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperPrefix .aa-LoadingIndicator[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapper,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper{order:3;position:relative;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input{appearance:none;background:none;border:0;color:#373a3c;font:inherit;height:calc(1.5em + .1rem + 2px);padding:0;width:100%}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::placeholder,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::placeholder{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input:focus{border-color:none;box-shadow:none;outline:none}.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-Autocomplete .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-decoration,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-cancel-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-button,.aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input::-webkit-search-results-decoration{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix{align-items:center;display:flex;order:4}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton{align-items:center;background:none;border:0;color:#373a3c;opacity:.8;cursor:pointer;display:flex;margin:0;width:calc(1.5em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton:focus{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton[hidden]{display:none}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-ClearButton svg{width:calc(1.5em + 0.75rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton{border:none;align-items:center;background:none;color:#373a3c;opacity:.4;font-size:.7rem;cursor:pointer;display:none;margin:0;width:calc(1em + .1rem + 2px)}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:hover,.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton:focus{color:#373a3c;opacity:.8}.aa-Autocomplete .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden],.aa-DetachedFormContainer .aa-Form .aa-InputWrapperSuffix .aa-CopyButton[hidden]{display:none}.aa-PanelLayout:empty{display:none}.quarto-search-no-results.no-query{display:none}.aa-Source:has(.no-query){display:none}#quarto-search-results .aa-Panel{border:solid #ced4da 1px}#quarto-search-results .aa-SourceNoResults{width:398px}.aa-DetachedOverlay .aa-Panel,#quarto-search-results .aa-Panel{max-height:65vh;overflow-y:auto;font-size:.925rem}.aa-DetachedOverlay .aa-SourceNoResults,#quarto-search-results .aa-SourceNoResults{height:60px;display:flex;justify-content:center;align-items:center}.aa-DetachedOverlay .search-error,#quarto-search-results .search-error{padding-top:10px;padding-left:20px;padding-right:20px;cursor:default}.aa-DetachedOverlay .search-error .search-error-title,#quarto-search-results .search-error .search-error-title{font-size:1.1rem;margin-bottom:.5rem}.aa-DetachedOverlay .search-error .search-error-title .search-error-icon,#quarto-search-results .search-error .search-error-title .search-error-icon{margin-right:8px}.aa-DetachedOverlay .search-error .search-error-text,#quarto-search-results .search-error .search-error-text{font-weight:300}.aa-DetachedOverlay .search-result-text,#quarto-search-results .search-result-text{font-weight:300;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.2rem;max-height:2.4rem}.aa-DetachedOverlay .aa-SourceHeader .search-result-header,#quarto-search-results .aa-SourceHeader .search-result-header{font-size:.875rem;background-color:#f2f2f2;padding-left:14px;padding-bottom:4px;padding-top:4px}.aa-DetachedOverlay .aa-SourceHeader .search-result-header-no-results,#quarto-search-results .aa-SourceHeader .search-result-header-no-results{display:none}.aa-DetachedOverlay .aa-SourceFooter .algolia-search-logo,#quarto-search-results .aa-SourceFooter .algolia-search-logo{width:110px;opacity:.85;margin:8px;float:right}.aa-DetachedOverlay .search-result-section,#quarto-search-results .search-result-section{font-size:.925em}.aa-DetachedOverlay a.search-result-link,#quarto-search-results a.search-result-link{color:inherit;text-decoration:none}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item,#quarto-search-results li.aa-Item[aria-selected=true] .search-item{background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-result-text-container{color:#fff;background-color:#2780e3}.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=true] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=true] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=true] .search-item .search-match.mark{color:#fff;background-color:#4b95e8}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item,#quarto-search-results li.aa-Item[aria-selected=false] .search-item{background-color:#fff}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item.search-result-more,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-section,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-title-container,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-result-text-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item.search-result-more,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-section,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-title-container,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-result-text-container{color:#373a3c}.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item mark.search-match,.aa-DetachedOverlay li.aa-Item[aria-selected=false] .search-item .search-match.mark,#quarto-search-results li.aa-Item[aria-selected=false] .search-item mark.search-match,#quarto-search-results li.aa-Item[aria-selected=false] .search-item .search-match.mark{color:inherit;background-color:#e5effc}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-title-container{background-color:#fff;color:#373a3c}.aa-DetachedOverlay .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container,#quarto-search-results .aa-Item .search-result-doc:not(.document-selectable) .search-result-text-container{padding-top:0px}.aa-DetachedOverlay li.aa-Item .search-result-doc.document-selectable .search-result-text-container,#quarto-search-results li.aa-Item .search-result-doc.document-selectable .search-result-text-container{margin-top:-4px}.aa-DetachedOverlay .aa-Item,#quarto-search-results .aa-Item{cursor:pointer}.aa-DetachedOverlay .aa-Item .search-item,#quarto-search-results .aa-Item .search-item{border-left:none;border-right:none;border-top:none;background-color:#fff;border-color:#ced4da;color:#373a3c}.aa-DetachedOverlay .aa-Item .search-item p,#quarto-search-results .aa-Item .search-item p{margin-top:0;margin-bottom:0}.aa-DetachedOverlay .aa-Item .search-item i.bi,#quarto-search-results .aa-Item .search-item i.bi{padding-left:8px;padding-right:8px;font-size:1.3em}.aa-DetachedOverlay .aa-Item .search-item .search-result-title,#quarto-search-results .aa-Item .search-item .search-result-title{margin-top:.3em;margin-bottom:.1rem}.aa-DetachedOverlay .aa-Item .search-result-title-container,#quarto-search-results .aa-Item .search-result-title-container{font-size:1em;display:flex;padding:6px 4px 6px 4px}.aa-DetachedOverlay .aa-Item .search-result-text-container,#quarto-search-results .aa-Item .search-result-text-container{padding-bottom:8px;padding-right:8px;margin-left:44px}.aa-DetachedOverlay .aa-Item .search-result-doc-section,.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-doc-section,#quarto-search-results .aa-Item .search-result-more{padding-top:8px;padding-bottom:8px;padding-left:44px}.aa-DetachedOverlay .aa-Item .search-result-more,#quarto-search-results .aa-Item .search-result-more{font-size:.8em;font-weight:400}.aa-DetachedOverlay .aa-Item .search-result-doc,#quarto-search-results .aa-Item .search-result-doc{border-top:1px solid #ced4da}.aa-DetachedSearchButton{background:none;border:none}.aa-DetachedSearchButton .aa-DetachedSearchButtonPlaceholder{display:none}.navbar .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#545555}.sidebar-tools-collapse #quarto-search,.sidebar-tools-main #quarto-search{display:inline}.sidebar-tools-collapse #quarto-search .aa-Autocomplete,.sidebar-tools-main #quarto-search .aa-Autocomplete{display:inline}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton{padding-left:4px;padding-right:4px}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon{color:#595959}.sidebar-tools-collapse #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon,.sidebar-tools-main #quarto-search .aa-DetachedSearchButton .aa-DetachedSearchButtonIcon .aa-SubmitIcon{margin-top:-3px}.aa-DetachedContainer{background:rgba(255,255,255,.65);width:90%;bottom:0;box-shadow:rgba(206,212,218,.6) 0 0 0 1px;outline:currentColor none medium;display:flex;flex-direction:column;left:0;margin:0;overflow:hidden;padding:0;position:fixed;right:0;top:0;z-index:1101}.aa-DetachedContainer::after{height:32px}.aa-DetachedContainer .aa-SourceHeader{margin:var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px}.aa-DetachedContainer .aa-Panel{background-color:#fff;border-radius:0;box-shadow:none;flex-grow:1;margin:0;padding:0;position:relative}.aa-DetachedContainer .aa-PanelLayout{bottom:0;box-shadow:none;left:0;margin:0;max-height:none;overflow-y:auto;position:absolute;right:0;top:0;width:100%}.aa-DetachedFormContainer{background-color:#fff;border-bottom:1px solid #ced4da;display:flex;flex-direction:row;justify-content:space-between;margin:0;padding:.5em}.aa-DetachedCancelButton{background:none;font-size:.8em;border:0;border-radius:3px;color:#373a3c;cursor:pointer;margin:0 0 0 .5em;padding:0 .5em}.aa-DetachedCancelButton:hover,.aa-DetachedCancelButton:focus{box-shadow:rgba(39,128,227,.6) 0 0 0 1px;outline:currentColor none medium}.aa-DetachedContainer--modal{bottom:inherit;height:auto;margin:0 auto;position:absolute;top:100px;border-radius:6px;max-width:850px}@media(max-width: 575.98px){.aa-DetachedContainer--modal{width:100%;top:0px;border-radius:0px;border:none}}.aa-DetachedContainer--modal .aa-PanelLayout{max-height:var(--aa-detached-modal-max-height);padding-bottom:var(--aa-spacing-half);position:static}.aa-Detached{height:100vh;overflow:hidden}.aa-DetachedOverlay{background-color:rgba(55,58,60,.4);position:fixed;left:0;right:0;top:0;margin:0;padding:0;height:100vh;z-index:1100}.quarto-listing{padding-bottom:1em}.listing-pagination{padding-top:.5em}ul.pagination{float:right;padding-left:8px;padding-top:.5em}ul.pagination li{padding-right:.75em}ul.pagination li.disabled a,ul.pagination li.active a{color:#373a3c;text-decoration:none}ul.pagination li:last-of-type{padding-right:0}.listing-actions-group{display:flex}.quarto-listing-filter{margin-bottom:1em;width:200px;margin-left:auto}.quarto-listing-sort{margin-bottom:1em;margin-right:auto;width:auto}.quarto-listing-sort .input-group-text{font-size:.8em}.input-group-text{border-right:none}.quarto-listing-sort select.form-select{font-size:.8em}.listing-no-matching{text-align:center;padding-top:2em;padding-bottom:3em;font-size:1em}#quarto-margin-sidebar .quarto-listing-category{padding-top:0;font-size:1rem}#quarto-margin-sidebar .quarto-listing-category-title{cursor:pointer;font-weight:600;font-size:1rem}.quarto-listing-category .category{cursor:pointer}.quarto-listing-category .category.active{font-weight:600}.quarto-listing-category.category-cloud{display:flex;flex-wrap:wrap;align-items:baseline}.quarto-listing-category.category-cloud .category{padding-right:5px}.quarto-listing-category.category-cloud .category-cloud-1{font-size:.75em}.quarto-listing-category.category-cloud .category-cloud-2{font-size:.95em}.quarto-listing-category.category-cloud .category-cloud-3{font-size:1.15em}.quarto-listing-category.category-cloud .category-cloud-4{font-size:1.35em}.quarto-listing-category.category-cloud .category-cloud-5{font-size:1.55em}.quarto-listing-category.category-cloud .category-cloud-6{font-size:1.75em}.quarto-listing-category.category-cloud .category-cloud-7{font-size:1.95em}.quarto-listing-category.category-cloud .category-cloud-8{font-size:2.15em}.quarto-listing-category.category-cloud .category-cloud-9{font-size:2.35em}.quarto-listing-category.category-cloud .category-cloud-10{font-size:2.55em}.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-1{grid-template-columns:repeat(1, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-1{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-2{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-2{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-3{grid-template-columns:repeat(3, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-3{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-3{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-4{grid-template-columns:repeat(4, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-4{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-4{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-5{grid-template-columns:repeat(5, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-5{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-5{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-6{grid-template-columns:repeat(6, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-6{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-6{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-7{grid-template-columns:repeat(7, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-7{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-7{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-8{grid-template-columns:repeat(8, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-8{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-8{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-9{grid-template-columns:repeat(9, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-9{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-9{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-10{grid-template-columns:repeat(10, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-10{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-10{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-11{grid-template-columns:repeat(11, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-11{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-11{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-cols-12{grid-template-columns:repeat(12, minmax(0, 1fr));gap:1.5em}@media(max-width: 767.98px){.quarto-listing-cols-12{grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.5em}}@media(max-width: 575.98px){.quarto-listing-cols-12{grid-template-columns:minmax(0, 1fr);gap:1.5em}}.quarto-listing-grid{gap:1.5em}.quarto-grid-item.borderless{border:none}.quarto-grid-item.borderless .listing-categories .listing-category:last-of-type,.quarto-grid-item.borderless .listing-categories .listing-category:first-of-type{padding-left:0}.quarto-grid-item.borderless .listing-categories .listing-category{border:0}.quarto-grid-link{text-decoration:none;color:inherit}.quarto-grid-link:hover{text-decoration:none;color:inherit}.quarto-grid-item h5.title,.quarto-grid-item .title.h5{margin-top:0;margin-bottom:0}.quarto-grid-item .card-footer{display:flex;justify-content:space-between;font-size:.8em}.quarto-grid-item .card-footer p{margin-bottom:0}.quarto-grid-item p.card-img-top{margin-bottom:0}.quarto-grid-item p.card-img-top>img{object-fit:cover}.quarto-grid-item .card-other-values{margin-top:.5em;font-size:.8em}.quarto-grid-item .card-other-values tr{margin-bottom:.5em}.quarto-grid-item .card-other-values tr>td:first-of-type{font-weight:600;padding-right:1em;padding-left:1em;vertical-align:top}.quarto-grid-item div.post-contents{display:flex;flex-direction:column;text-decoration:none;height:100%}.quarto-grid-item .listing-item-img-placeholder{background-color:#adb5bd;flex-shrink:0}.quarto-grid-item .card-attribution{padding-top:1em;display:flex;gap:1em;text-transform:uppercase;color:#6c757d;font-weight:500;flex-grow:10;align-items:flex-end}.quarto-grid-item .description{padding-bottom:1em}.quarto-grid-item .card-attribution .date{align-self:flex-end}.quarto-grid-item .card-attribution.justify{justify-content:space-between}.quarto-grid-item .card-attribution.start{justify-content:flex-start}.quarto-grid-item .card-attribution.end{justify-content:flex-end}.quarto-grid-item .card-title{margin-bottom:.1em}.quarto-grid-item .card-subtitle{padding-top:.25em}.quarto-grid-item .card-text{font-size:.9em}.quarto-grid-item .listing-reading-time{padding-bottom:.25em}.quarto-grid-item .card-text-small{font-size:.8em}.quarto-grid-item .card-subtitle.subtitle{font-size:.9em;font-weight:600;padding-bottom:.5em}.quarto-grid-item .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}.quarto-grid-item .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}.quarto-grid-item.card-right{text-align:right}.quarto-grid-item.card-right .listing-categories{justify-content:flex-end}.quarto-grid-item.card-left{text-align:left}.quarto-grid-item.card-center{text-align:center}.quarto-grid-item.card-center .listing-description{text-align:justify}.quarto-grid-item.card-center .listing-categories{justify-content:center}table.quarto-listing-table td.image{padding:0px}table.quarto-listing-table td.image img{width:100%;max-width:50px;object-fit:contain}table.quarto-listing-table a{text-decoration:none}table.quarto-listing-table th a{color:inherit}table.quarto-listing-table th a.asc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table th a.desc:after{margin-bottom:-2px;margin-left:5px;display:inline-block;height:1rem;width:1rem;background-repeat:no-repeat;background-size:1rem 1rem;background-image:url('data:image/svg+xml,');content:""}table.quarto-listing-table.table-hover td{cursor:pointer}.quarto-post.image-left{flex-direction:row}.quarto-post.image-right{flex-direction:row-reverse}@media(max-width: 767.98px){.quarto-post.image-right,.quarto-post.image-left{gap:0em;flex-direction:column}.quarto-post .metadata{padding-bottom:1em;order:2}.quarto-post .body{order:1}.quarto-post .thumbnail{order:3}}.list.quarto-listing-default div:last-of-type{border-bottom:none}@media(min-width: 992px){.quarto-listing-container-default{margin-right:2em}}div.quarto-post{display:flex;gap:2em;margin-bottom:1.5em;border-bottom:1px solid #dee2e6}@media(max-width: 767.98px){div.quarto-post{padding-bottom:1em}}div.quarto-post .metadata{flex-basis:20%;flex-grow:0;margin-top:.2em;flex-shrink:10}div.quarto-post .thumbnail{flex-basis:30%;flex-grow:0;flex-shrink:0}div.quarto-post .thumbnail img{margin-top:.4em;width:100%;object-fit:cover}div.quarto-post .body{flex-basis:45%;flex-grow:1;flex-shrink:0}div.quarto-post .body h3.listing-title,div.quarto-post .body .listing-title.h3{margin-top:0px;margin-bottom:0px;border-bottom:none}div.quarto-post .body .listing-subtitle{font-size:.875em;margin-bottom:.5em;margin-top:.2em}div.quarto-post .body .description{font-size:.9em}div.quarto-post a{color:#373a3c;display:flex;flex-direction:column;text-decoration:none}div.quarto-post a div.description{flex-shrink:0}div.quarto-post .metadata{display:flex;flex-direction:column;font-size:.8em;font-family:var(--bs-font-sans-serif);flex-basis:33%}div.quarto-post .listing-categories{display:flex;flex-wrap:wrap;padding-bottom:5px}div.quarto-post .listing-categories .listing-category{color:#6c757d;border:solid 1px #dee2e6;border-radius:.25rem;text-transform:uppercase;font-size:.65em;padding-left:.5em;padding-right:.5em;padding-top:.15em;padding-bottom:.15em;cursor:pointer;margin-right:4px;margin-bottom:4px}div.quarto-post .listing-description{margin-bottom:.5em}div.quarto-about-jolla{display:flex !important;flex-direction:column;align-items:center;margin-top:10%;padding-bottom:1em}div.quarto-about-jolla .about-image{object-fit:cover;margin-left:auto;margin-right:auto;margin-bottom:1.5em}div.quarto-about-jolla img.round{border-radius:50%}div.quarto-about-jolla img.rounded{border-radius:10px}div.quarto-about-jolla .quarto-title h1.title,div.quarto-about-jolla .quarto-title .title.h1{text-align:center}div.quarto-about-jolla .quarto-title .description{text-align:center}div.quarto-about-jolla h2,div.quarto-about-jolla .h2{border-bottom:none}div.quarto-about-jolla .about-sep{width:60%}div.quarto-about-jolla main{text-align:center}div.quarto-about-jolla .about-links{display:flex}@media(min-width: 992px){div.quarto-about-jolla .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-jolla .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-jolla .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-jolla .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-jolla .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-jolla .about-link:hover{color:#2780e3}div.quarto-about-jolla .about-link i.bi{margin-right:.15em}div.quarto-about-solana{display:flex !important;flex-direction:column;padding-top:3em !important;padding-bottom:1em}div.quarto-about-solana .about-entity{display:flex !important;align-items:start;justify-content:space-between}@media(min-width: 992px){div.quarto-about-solana .about-entity{flex-direction:row}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity{flex-direction:column-reverse;align-items:center;text-align:center}}div.quarto-about-solana .about-entity .entity-contents{display:flex;flex-direction:column}@media(max-width: 767.98px){div.quarto-about-solana .about-entity .entity-contents{width:100%}}div.quarto-about-solana .about-entity .about-image{object-fit:cover}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-image{margin-bottom:1.5em}}div.quarto-about-solana .about-entity img.round{border-radius:50%}div.quarto-about-solana .about-entity img.rounded{border-radius:10px}div.quarto-about-solana .about-entity .about-links{display:flex;justify-content:left;padding-bottom:1.2em}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-solana .about-entity .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-solana .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-solana .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-solana .about-entity .about-link:hover{color:#2780e3}div.quarto-about-solana .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-solana .about-contents{padding-right:1.5em;flex-basis:0;flex-grow:1}div.quarto-about-solana .about-contents main.content{margin-top:0}div.quarto-about-solana .about-contents h2,div.quarto-about-solana .about-contents .h2{border-bottom:none}div.quarto-about-trestles{display:flex !important;flex-direction:row;padding-top:3em !important;padding-bottom:1em}@media(max-width: 991.98px){div.quarto-about-trestles{flex-direction:column;padding-top:0em !important}}div.quarto-about-trestles .about-entity{display:flex !important;flex-direction:column;align-items:center;text-align:center;padding-right:1em}@media(min-width: 992px){div.quarto-about-trestles .about-entity{flex:0 0 42%}}div.quarto-about-trestles .about-entity .about-image{object-fit:cover;margin-bottom:1.5em}div.quarto-about-trestles .about-entity img.round{border-radius:50%}div.quarto-about-trestles .about-entity img.rounded{border-radius:10px}div.quarto-about-trestles .about-entity .about-links{display:flex;justify-content:center}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-trestles .about-entity .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-trestles .about-entity .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-trestles .about-entity .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-trestles .about-entity .about-link:hover{color:#2780e3}div.quarto-about-trestles .about-entity .about-link i.bi{margin-right:.15em}div.quarto-about-trestles .about-contents{flex-basis:0;flex-grow:1}div.quarto-about-trestles .about-contents h2,div.quarto-about-trestles .about-contents .h2{border-bottom:none}@media(min-width: 992px){div.quarto-about-trestles .about-contents{border-left:solid 1px #dee2e6;padding-left:1.5em}}div.quarto-about-trestles .about-contents main.content{margin-top:0}div.quarto-about-marquee{padding-bottom:1em}div.quarto-about-marquee .about-contents{display:flex;flex-direction:column}div.quarto-about-marquee .about-image{max-height:550px;margin-bottom:1.5em;object-fit:cover}div.quarto-about-marquee img.round{border-radius:50%}div.quarto-about-marquee img.rounded{border-radius:10px}div.quarto-about-marquee h2,div.quarto-about-marquee .h2{border-bottom:none}div.quarto-about-marquee .about-links{display:flex;justify-content:center;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-marquee .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-marquee .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-marquee .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-marquee .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-marquee .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-marquee .about-link:hover{color:#2780e3}div.quarto-about-marquee .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-marquee .about-link{border:none}}div.quarto-about-broadside{display:flex;flex-direction:column;padding-bottom:1em}div.quarto-about-broadside .about-main{display:flex !important;padding-top:0 !important}@media(min-width: 992px){div.quarto-about-broadside .about-main{flex-direction:row;align-items:flex-start}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main{flex-direction:column}}@media(max-width: 991.98px){div.quarto-about-broadside .about-main .about-entity{flex-shrink:0;width:100%;height:450px;margin-bottom:1.5em;background-size:cover;background-repeat:no-repeat}}@media(min-width: 992px){div.quarto-about-broadside .about-main .about-entity{flex:0 10 50%;margin-right:1.5em;width:100%;height:100%;background-size:100%;background-repeat:no-repeat}}div.quarto-about-broadside .about-main .about-contents{padding-top:14px;flex:0 0 50%}div.quarto-about-broadside h2,div.quarto-about-broadside .h2{border-bottom:none}div.quarto-about-broadside .about-sep{margin-top:1.5em;width:60%;align-self:center}div.quarto-about-broadside .about-links{display:flex;justify-content:center;column-gap:20px;padding-top:1.5em}@media(min-width: 992px){div.quarto-about-broadside .about-links{flex-direction:row;column-gap:.8em;row-gap:15px;flex-wrap:wrap}}@media(max-width: 991.98px){div.quarto-about-broadside .about-links{flex-direction:column;row-gap:1em;width:100%;padding-bottom:1.5em}}div.quarto-about-broadside .about-link{color:#686d71;text-decoration:none;border:solid 1px}@media(min-width: 992px){div.quarto-about-broadside .about-link{font-size:.8em;padding:.25em .5em;border-radius:4px}}@media(max-width: 991.98px){div.quarto-about-broadside .about-link{font-size:1.1em;padding:.5em .5em;text-align:center;border-radius:6px}}div.quarto-about-broadside .about-link:hover{color:#2780e3}div.quarto-about-broadside .about-link i.bi{margin-right:.15em}@media(min-width: 992px){div.quarto-about-broadside .about-link{border:none}}.tippy-box[data-theme~=quarto]{background-color:#fff;border:solid 1px #dee2e6;border-radius:.25rem;color:#373a3c;font-size:.875rem}.tippy-box[data-theme~=quarto]>.tippy-backdrop{background-color:#fff}.tippy-box[data-theme~=quarto]>.tippy-arrow:after,.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{content:"";position:absolute;z-index:-1}.tippy-box[data-theme~=quarto]>.tippy-arrow:after{border-color:rgba(0,0,0,0);border-style:solid}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-6px}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-6px}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-6px}.tippy-box[data-placement^=left]>.tippy-arrow:before{right:-6px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:before{border-top-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-arrow:after{border-top-color:#dee2e6;border-width:7px 7px 0;top:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow>svg{top:16px}.tippy-box[data-theme~=quarto][data-placement^=top]>.tippy-svg-arrow:after{top:17px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:before{border-bottom-color:#fff;bottom:16px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:#dee2e6;border-width:0 7px 7px;bottom:17px;left:1px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:15px}.tippy-box[data-theme~=quarto][data-placement^=bottom]>.tippy-svg-arrow:after{bottom:17px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:before{border-left-color:#fff}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-arrow:after{border-left-color:#dee2e6;border-width:7px 0 7px 7px;left:17px;top:1px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow>svg{left:11px}.tippy-box[data-theme~=quarto][data-placement^=left]>.tippy-svg-arrow:after{left:12px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:before{border-right-color:#fff;right:16px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-arrow:after{border-width:7px 7px 7px 0;right:17px;top:1px;border-right-color:#dee2e6}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow>svg{right:11px}.tippy-box[data-theme~=quarto][data-placement^=right]>.tippy-svg-arrow:after{right:12px}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow{fill:#373a3c}.tippy-box[data-theme~=quarto]>.tippy-svg-arrow:after{background-image:url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMCA2czEuNzk2LS4wMTMgNC42Ny0zLjYxNUM1Ljg1MS45IDYuOTMuMDA2IDggMGMxLjA3LS4wMDYgMi4xNDguODg3IDMuMzQzIDIuMzg1QzE0LjIzMyA2LjAwNSAxNiA2IDE2IDZIMHoiIGZpbGw9InJnYmEoMCwgOCwgMTYsIDAuMikiLz48L3N2Zz4=);background-size:16px 6px;width:16px;height:6px}.top-right{position:absolute;top:1em;right:1em}.hidden{display:none !important}.zindex-bottom{z-index:-1 !important}.quarto-layout-panel{margin-bottom:1em}.quarto-layout-panel>figure{width:100%}.quarto-layout-panel>figure>figcaption,.quarto-layout-panel>.panel-caption{margin-top:10pt}.quarto-layout-panel>.table-caption{margin-top:0px}.table-caption p{margin-bottom:.5em}.quarto-layout-row{display:flex;flex-direction:row;align-items:flex-start}.quarto-layout-valign-top{align-items:flex-start}.quarto-layout-valign-bottom{align-items:flex-end}.quarto-layout-valign-center{align-items:center}.quarto-layout-cell{position:relative;margin-right:20px}.quarto-layout-cell:last-child{margin-right:0}.quarto-layout-cell figure,.quarto-layout-cell>p{margin:.2em}.quarto-layout-cell img{max-width:100%}.quarto-layout-cell .html-widget{width:100% !important}.quarto-layout-cell div figure p{margin:0}.quarto-layout-cell figure{display:inline-block;margin-inline-start:0;margin-inline-end:0}.quarto-layout-cell table{display:inline-table}.quarto-layout-cell-subref figcaption,figure .quarto-layout-row figure figcaption{text-align:center;font-style:italic}.quarto-figure{position:relative;margin-bottom:1em}.quarto-figure>figure{width:100%;margin-bottom:0}.quarto-figure-left>figure>p,.quarto-figure-left>figure>div{text-align:left}.quarto-figure-center>figure>p,.quarto-figure-center>figure>div{text-align:center}.quarto-figure-right>figure>p,.quarto-figure-right>figure>div{text-align:right}figure>p:empty{display:none}figure>p:first-child{margin-top:0;margin-bottom:0}figure>figcaption{margin-top:.5em}div[id^=tbl-]{position:relative}.quarto-figure>.anchorjs-link{position:absolute;top:.6em;right:.5em}div[id^=tbl-]>.anchorjs-link{position:absolute;top:.7em;right:.3em}.quarto-figure:hover>.anchorjs-link,div[id^=tbl-]:hover>.anchorjs-link,h2:hover>.anchorjs-link,.h2:hover>.anchorjs-link,h3:hover>.anchorjs-link,.h3:hover>.anchorjs-link,h4:hover>.anchorjs-link,.h4:hover>.anchorjs-link,h5:hover>.anchorjs-link,.h5:hover>.anchorjs-link,h6:hover>.anchorjs-link,.h6:hover>.anchorjs-link,.reveal-anchorjs-link>.anchorjs-link{opacity:1}#title-block-header{margin-block-end:1rem;position:relative;margin-top:-1px}#title-block-header .abstract{margin-block-start:1rem}#title-block-header .abstract .abstract-title{font-weight:600}#title-block-header a{text-decoration:none}#title-block-header .author,#title-block-header .date,#title-block-header .doi{margin-block-end:.2rem}#title-block-header .quarto-title-block>div{display:flex}#title-block-header .quarto-title-block>div>h1,#title-block-header .quarto-title-block>div>.h1{flex-grow:1}#title-block-header .quarto-title-block>div>button{flex-shrink:0;height:2.25rem;margin-top:0}@media(min-width: 992px){#title-block-header .quarto-title-block>div>button{margin-top:5px}}tr.header>th>p:last-of-type{margin-bottom:0px}table,.table{caption-side:top;margin-bottom:1.5rem}caption,.table-caption{padding-top:.5rem;padding-bottom:.5rem;text-align:center}.utterances{max-width:none;margin-left:-8px}iframe{margin-bottom:1em}details{margin-bottom:1em}details[show]{margin-bottom:0}details>summary{color:#6c757d}details>summary>p:only-child{display:inline}pre.sourceCode,code.sourceCode{position:relative}p code:not(.sourceCode){white-space:pre-wrap}code{white-space:pre}@media print{code{white-space:pre-wrap}}pre>code{display:block}pre>code.sourceCode{white-space:pre}pre>code.sourceCode>span>a:first-child::before{text-decoration:none}pre.code-overflow-wrap>code.sourceCode{white-space:pre-wrap}pre.code-overflow-scroll>code.sourceCode{white-space:pre}code a:any-link{color:inherit;text-decoration:none}code a:hover{color:inherit;text-decoration:underline}ul.task-list{padding-left:1em}[data-tippy-root]{display:inline-block}.tippy-content .footnote-back{display:none}.quarto-embedded-source-code{display:none}.quarto-unresolved-ref{font-weight:600}.quarto-cover-image{max-width:35%;float:right;margin-left:30px}.cell-output-display .widget-subarea{margin-bottom:1em}.cell-output-display:not(.no-overflow-x),.knitsql-table:not(.no-overflow-x){overflow-x:auto}.panel-input{margin-bottom:1em}.panel-input>div,.panel-input>div>div{display:inline-block;vertical-align:top;padding-right:12px}.panel-input>p:last-child{margin-bottom:0}.layout-sidebar{margin-bottom:1em}.layout-sidebar .tab-content{border:none}.tab-content>.page-columns.active{display:grid}div.sourceCode>iframe{width:100%;height:300px;margin-bottom:-0.5em}div.ansi-escaped-output{font-family:monospace;display:block}/*! +* +* ansi colors from IPython notebook's +* +*/.ansi-black-fg{color:#3e424d}.ansi-black-bg{background-color:#3e424d}.ansi-black-intense-fg{color:#282c36}.ansi-black-intense-bg{background-color:#282c36}.ansi-red-fg{color:#e75c58}.ansi-red-bg{background-color:#e75c58}.ansi-red-intense-fg{color:#b22b31}.ansi-red-intense-bg{background-color:#b22b31}.ansi-green-fg{color:#00a250}.ansi-green-bg{background-color:#00a250}.ansi-green-intense-fg{color:#007427}.ansi-green-intense-bg{background-color:#007427}.ansi-yellow-fg{color:#ddb62b}.ansi-yellow-bg{background-color:#ddb62b}.ansi-yellow-intense-fg{color:#b27d12}.ansi-yellow-intense-bg{background-color:#b27d12}.ansi-blue-fg{color:#208ffb}.ansi-blue-bg{background-color:#208ffb}.ansi-blue-intense-fg{color:#0065ca}.ansi-blue-intense-bg{background-color:#0065ca}.ansi-magenta-fg{color:#d160c4}.ansi-magenta-bg{background-color:#d160c4}.ansi-magenta-intense-fg{color:#a03196}.ansi-magenta-intense-bg{background-color:#a03196}.ansi-cyan-fg{color:#60c6c8}.ansi-cyan-bg{background-color:#60c6c8}.ansi-cyan-intense-fg{color:#258f8f}.ansi-cyan-intense-bg{background-color:#258f8f}.ansi-white-fg{color:#c5c1b4}.ansi-white-bg{background-color:#c5c1b4}.ansi-white-intense-fg{color:#a1a6b2}.ansi-white-intense-bg{background-color:#a1a6b2}.ansi-default-inverse-fg{color:#fff}.ansi-default-inverse-bg{background-color:#000}.ansi-bold{font-weight:bold}.ansi-underline{text-decoration:underline}:root{--quarto-body-bg: #fff;--quarto-body-color: #373a3c;--quarto-text-muted: #6c757d;--quarto-border-color: #dee2e6;--quarto-border-width: 1px;--quarto-border-radius: 0.25rem}table.gt_table{color:var(--quarto-body-color);font-size:1em;width:100%;background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_column_spanner_outer{color:var(--quarto-body-color);background-color:rgba(0,0,0,0);border-top-width:inherit;border-bottom-width:inherit;border-color:var(--quarto-border-color)}table.gt_table th.gt_col_heading{color:var(--quarto-body-color);font-weight:bold;background-color:rgba(0,0,0,0)}table.gt_table thead.gt_col_headings{border-bottom:1px solid currentColor;border-top-width:inherit;border-top-color:var(--quarto-border-color)}table.gt_table thead.gt_col_headings:not(:first-child){border-top-width:1px;border-top-color:var(--quarto-border-color)}table.gt_table td.gt_row{border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-width:0px}table.gt_table tbody.gt_table_body{border-top-width:1px;border-bottom-width:1px;border-bottom-color:var(--quarto-border-color);border-top-color:currentColor}div.columns{display:initial;gap:initial}div.column{display:inline-block;overflow-x:initial;vertical-align:top;width:50%}.code-annotation-tip-content{word-wrap:break-word}.code-annotation-container-hidden{display:none !important}dl.code-annotation-container-grid{display:grid;grid-template-columns:min-content auto}dl.code-annotation-container-grid dt{grid-column:1}dl.code-annotation-container-grid dd{grid-column:2}pre.sourceCode.code-annotation-code{padding-right:0}code.sourceCode .code-annotation-anchor{z-index:100;position:absolute;right:.5em;left:inherit;background-color:rgba(0,0,0,0)}:root{--mermaid-bg-color: #fff;--mermaid-edge-color: #373a3c;--mermaid-node-fg-color: #373a3c;--mermaid-fg-color: #373a3c;--mermaid-fg-color--lighter: #4f5457;--mermaid-fg-color--lightest: #686d71;--mermaid-font-family: Source Sans Pro, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;--mermaid-label-bg-color: #fff;--mermaid-label-fg-color: #2780e3;--mermaid-node-bg-color: rgba(39, 128, 227, 0.1);--mermaid-node-fg-color: #373a3c}@media print{:root{font-size:11pt}#quarto-sidebar,#TOC,.nav-page{display:none}.page-columns .content{grid-column-start:page-start}.fixed-top{position:relative}.panel-caption,.figure-caption,figcaption{color:#666}}.code-copy-button{position:absolute;top:0;right:0;border:0;margin-top:5px;margin-right:5px;background-color:rgba(0,0,0,0);z-index:3}.code-copy-button:focus{outline:none}.code-copy-button-tooltip{font-size:.75em}pre.sourceCode:hover>.code-copy-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}pre.sourceCode:hover>.code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}pre.sourceCode:hover>.code-copy-button-checked:hover>.bi::before{background-image:url('data:image/svg+xml,')}main ol ol,main ul ul,main ol ul,main ul ol{margin-bottom:1em}ul>li:not(:has(>p))>ul,ol>li:not(:has(>p))>ul,ul>li:not(:has(>p))>ol,ol>li:not(:has(>p))>ol{margin-bottom:0}ul>li:not(:has(>p))>ul>li:has(>p),ol>li:not(:has(>p))>ul>li:has(>p),ul>li:not(:has(>p))>ol>li:has(>p),ol>li:not(:has(>p))>ol>li:has(>p){margin-top:1rem}body{margin:0}main.page-columns>header>h1.title,main.page-columns>header>.title.h1{margin-bottom:0}@media(min-width: 992px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] 35px [page-end-inset page-end] 5fr [screen-end-inset] 1.5em}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset] 35px [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 850px - 3em )) [body-content-end] 3em [body-end] 50px [body-end-outset] minmax(0px, 250px) [page-end-inset] minmax(50px, 100px) [page-end] 1fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 175px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 100px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start] minmax(50px, 100px) [page-start-inset] 50px [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(0px, 200px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 50px [page-start-inset] minmax(50px, 150px) [body-start-outset] 50px [body-start] 1.5em [body-content-start] minmax(450px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(50px, 150px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] minmax(25px, 50px) [page-start-inset] minmax(50px, 150px) [body-start-outset] minmax(25px, 50px) [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] minmax(25px, 50px) [body-end-outset] minmax(50px, 150px) [page-end-inset] minmax(25px, 50px) [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 991.98px){body .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.fullcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.slimcontent:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.listing:not(.floating):not(.docked) .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset] 5fr [body-start] 1.5em [body-content-start] minmax(500px, calc( 1250px - 3em )) [body-content-end body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start] 35px [page-start-inset] minmax(0px, 145px) [body-start-outset] 35px [body-start] 1.5em [body-content-start] minmax(450px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1.5em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 1000px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 800px - 3em )) [body-content-end] 1.5em [body-end body-end-outset page-end-inset page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.docked.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.docked.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(25px, 50px) [page-end-inset] 50px [page-end] 5fr [screen-end-inset] 1.5em [screen-end]}body.floating.slimcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 35px [body-end-outset] minmax(75px, 145px) [page-end-inset] 35px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}body.floating.listing .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset] 5fr [page-start page-start-inset body-start-outset body-start] 1em [body-content-start] minmax(500px, calc( 750px - 3em )) [body-content-end] 1.5em [body-end] 50px [body-end-outset] minmax(75px, 150px) [page-end-inset] 25px [page-end] 4fr [screen-end-inset] 1.5em [screen-end]}}@media(max-width: 767.98px){body .page-columns,body.fullcontent:not(.floating):not(.docked) .page-columns,body.slimcontent:not(.floating):not(.docked) .page-columns,body.docked .page-columns,body.docked.slimcontent .page-columns,body.docked.fullcontent .page-columns,body.floating .page-columns,body.floating.slimcontent .page-columns,body.floating.fullcontent .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}body:not(.floating):not(.docked) .page-columns.toc-left .page-columns{display:grid;gap:0;grid-template-columns:[screen-start] 1.5em [screen-start-inset page-start page-start-inset body-start-outset body-start body-content-start] minmax(0px, 1fr) [body-content-end body-end body-end-outset page-end-inset page-end screen-end-inset] 1.5em [screen-end]}nav[role=doc-toc]{display:none}}body,.page-row-navigation{grid-template-rows:[page-top] max-content [contents-top] max-content [contents-bottom] max-content [page-bottom]}.page-rows-contents{grid-template-rows:[content-top] minmax(max-content, 1fr) [content-bottom] minmax(60px, max-content) [page-bottom]}.page-full{grid-column:screen-start/screen-end !important}.page-columns>*{grid-column:body-content-start/body-content-end}.page-columns.column-page>*{grid-column:page-start/page-end}.page-columns.column-page-left>*{grid-column:page-start/body-content-end}.page-columns.column-page-right>*{grid-column:body-content-start/page-end}.page-rows{grid-auto-rows:auto}.header{grid-column:screen-start/screen-end;grid-row:page-top/contents-top}#quarto-content{padding:0;grid-column:screen-start/screen-end;grid-row:contents-top/contents-bottom}body.floating .sidebar.sidebar-navigation{grid-column:page-start/body-start;grid-row:content-top/page-bottom}body.docked .sidebar.sidebar-navigation{grid-column:screen-start/body-start;grid-row:content-top/page-bottom}.sidebar.toc-left{grid-column:page-start/body-start;grid-row:content-top/page-bottom}.sidebar.margin-sidebar{grid-column:body-end/page-end;grid-row:content-top/page-bottom}.page-columns .content{grid-column:body-content-start/body-content-end;grid-row:content-top/content-bottom;align-content:flex-start}.page-columns .page-navigation{grid-column:body-content-start/body-content-end;grid-row:content-bottom/page-bottom}.page-columns .footer{grid-column:screen-start/screen-end;grid-row:contents-bottom/page-bottom}.page-columns .column-body{grid-column:body-content-start/body-content-end}.page-columns .column-body-fullbleed{grid-column:body-start/body-end}.page-columns .column-body-outset{grid-column:body-start-outset/body-end-outset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset table{background:#fff}.page-columns .column-body-outset-left{grid-column:body-start-outset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset-left table{background:#fff}.page-columns .column-body-outset-right{grid-column:body-content-start/body-end-outset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-body-outset-right table{background:#fff}.page-columns .column-page{grid-column:page-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page table{background:#fff}.page-columns .column-page-inset{grid-column:page-start-inset/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset table{background:#fff}.page-columns .column-page-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset-left table{background:#fff}.page-columns .column-page-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-inset-right figcaption table{background:#fff}.page-columns .column-page-left{grid-column:page-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-left table{background:#fff}.page-columns .column-page-right{grid-column:body-content-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-page-right figcaption table{background:#fff}#quarto-content.page-columns #quarto-margin-sidebar,#quarto-content.page-columns #quarto-sidebar{z-index:1}@media(max-width: 991.98px){#quarto-content.page-columns #quarto-margin-sidebar.collapse,#quarto-content.page-columns #quarto-sidebar.collapse,#quarto-content.page-columns #quarto-margin-sidebar.collapsing,#quarto-content.page-columns #quarto-sidebar.collapsing{z-index:1055}}#quarto-content.page-columns main.column-page,#quarto-content.page-columns main.column-page-right,#quarto-content.page-columns main.column-page-left{z-index:0}.page-columns .column-screen-inset{grid-column:screen-start-inset/screen-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:screen-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/screen-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:screen-start/screen-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:screen-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/screen-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:screen-start/screen-end;padding:1em;background:#f8f9fa;z-index:998;transform:translate3d(0, 0, 0);margin-bottom:1em}.zindex-content{z-index:998;transform:translate3d(0, 0, 0)}.zindex-modal{z-index:1055;transform:translate3d(0, 0, 0)}.zindex-over-content{z-index:999;transform:translate3d(0, 0, 0)}img.img-fluid.column-screen,img.img-fluid.column-screen-inset-shaded,img.img-fluid.column-screen-inset,img.img-fluid.column-screen-inset-left,img.img-fluid.column-screen-inset-right,img.img-fluid.column-screen-left,img.img-fluid.column-screen-right{width:100%}@media(min-width: 992px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-end/page-end !important;z-index:998}.column-sidebar{grid-column:page-start/body-start !important;z-index:998}.column-leftmargin{grid-column:screen-start-inset/body-start !important;z-index:998}.no-row-height{height:1em;overflow:visible}}@media(max-width: 991.98px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-end/page-end !important;z-index:998}.no-row-height{height:1em;overflow:visible}.page-columns.page-full{overflow:visible}.page-columns.toc-left .margin-caption,.page-columns.toc-left div.aside,.page-columns.toc-left aside,.page-columns.toc-left .column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;transform:translate3d(0, 0, 0)}.page-columns.toc-left .no-row-height{height:initial;overflow:initial}}@media(max-width: 767.98px){.margin-caption,div.aside,aside,.column-margin{grid-column:body-content-start/body-content-end !important;z-index:998;transform:translate3d(0, 0, 0)}.no-row-height{height:initial;overflow:initial}#quarto-margin-sidebar{display:none}#quarto-sidebar-toc-left{display:none}.hidden-sm{display:none}}.panel-grid{display:grid;grid-template-rows:repeat(1, 1fr);grid-template-columns:repeat(24, 1fr);gap:1em}.panel-grid .g-col-1{grid-column:auto/span 1}.panel-grid .g-col-2{grid-column:auto/span 2}.panel-grid .g-col-3{grid-column:auto/span 3}.panel-grid .g-col-4{grid-column:auto/span 4}.panel-grid .g-col-5{grid-column:auto/span 5}.panel-grid .g-col-6{grid-column:auto/span 6}.panel-grid .g-col-7{grid-column:auto/span 7}.panel-grid .g-col-8{grid-column:auto/span 8}.panel-grid .g-col-9{grid-column:auto/span 9}.panel-grid .g-col-10{grid-column:auto/span 10}.panel-grid .g-col-11{grid-column:auto/span 11}.panel-grid .g-col-12{grid-column:auto/span 12}.panel-grid .g-col-13{grid-column:auto/span 13}.panel-grid .g-col-14{grid-column:auto/span 14}.panel-grid .g-col-15{grid-column:auto/span 15}.panel-grid .g-col-16{grid-column:auto/span 16}.panel-grid .g-col-17{grid-column:auto/span 17}.panel-grid .g-col-18{grid-column:auto/span 18}.panel-grid .g-col-19{grid-column:auto/span 19}.panel-grid .g-col-20{grid-column:auto/span 20}.panel-grid .g-col-21{grid-column:auto/span 21}.panel-grid .g-col-22{grid-column:auto/span 22}.panel-grid .g-col-23{grid-column:auto/span 23}.panel-grid .g-col-24{grid-column:auto/span 24}.panel-grid .g-start-1{grid-column-start:1}.panel-grid .g-start-2{grid-column-start:2}.panel-grid .g-start-3{grid-column-start:3}.panel-grid .g-start-4{grid-column-start:4}.panel-grid .g-start-5{grid-column-start:5}.panel-grid .g-start-6{grid-column-start:6}.panel-grid .g-start-7{grid-column-start:7}.panel-grid .g-start-8{grid-column-start:8}.panel-grid .g-start-9{grid-column-start:9}.panel-grid .g-start-10{grid-column-start:10}.panel-grid .g-start-11{grid-column-start:11}.panel-grid .g-start-12{grid-column-start:12}.panel-grid .g-start-13{grid-column-start:13}.panel-grid .g-start-14{grid-column-start:14}.panel-grid .g-start-15{grid-column-start:15}.panel-grid .g-start-16{grid-column-start:16}.panel-grid .g-start-17{grid-column-start:17}.panel-grid .g-start-18{grid-column-start:18}.panel-grid .g-start-19{grid-column-start:19}.panel-grid .g-start-20{grid-column-start:20}.panel-grid .g-start-21{grid-column-start:21}.panel-grid .g-start-22{grid-column-start:22}.panel-grid .g-start-23{grid-column-start:23}@media(min-width: 576px){.panel-grid .g-col-sm-1{grid-column:auto/span 1}.panel-grid .g-col-sm-2{grid-column:auto/span 2}.panel-grid .g-col-sm-3{grid-column:auto/span 3}.panel-grid .g-col-sm-4{grid-column:auto/span 4}.panel-grid .g-col-sm-5{grid-column:auto/span 5}.panel-grid .g-col-sm-6{grid-column:auto/span 6}.panel-grid .g-col-sm-7{grid-column:auto/span 7}.panel-grid .g-col-sm-8{grid-column:auto/span 8}.panel-grid .g-col-sm-9{grid-column:auto/span 9}.panel-grid .g-col-sm-10{grid-column:auto/span 10}.panel-grid .g-col-sm-11{grid-column:auto/span 11}.panel-grid .g-col-sm-12{grid-column:auto/span 12}.panel-grid .g-col-sm-13{grid-column:auto/span 13}.panel-grid .g-col-sm-14{grid-column:auto/span 14}.panel-grid .g-col-sm-15{grid-column:auto/span 15}.panel-grid .g-col-sm-16{grid-column:auto/span 16}.panel-grid .g-col-sm-17{grid-column:auto/span 17}.panel-grid .g-col-sm-18{grid-column:auto/span 18}.panel-grid .g-col-sm-19{grid-column:auto/span 19}.panel-grid .g-col-sm-20{grid-column:auto/span 20}.panel-grid .g-col-sm-21{grid-column:auto/span 21}.panel-grid .g-col-sm-22{grid-column:auto/span 22}.panel-grid .g-col-sm-23{grid-column:auto/span 23}.panel-grid .g-col-sm-24{grid-column:auto/span 24}.panel-grid .g-start-sm-1{grid-column-start:1}.panel-grid .g-start-sm-2{grid-column-start:2}.panel-grid .g-start-sm-3{grid-column-start:3}.panel-grid .g-start-sm-4{grid-column-start:4}.panel-grid .g-start-sm-5{grid-column-start:5}.panel-grid .g-start-sm-6{grid-column-start:6}.panel-grid .g-start-sm-7{grid-column-start:7}.panel-grid .g-start-sm-8{grid-column-start:8}.panel-grid .g-start-sm-9{grid-column-start:9}.panel-grid .g-start-sm-10{grid-column-start:10}.panel-grid .g-start-sm-11{grid-column-start:11}.panel-grid .g-start-sm-12{grid-column-start:12}.panel-grid .g-start-sm-13{grid-column-start:13}.panel-grid .g-start-sm-14{grid-column-start:14}.panel-grid .g-start-sm-15{grid-column-start:15}.panel-grid .g-start-sm-16{grid-column-start:16}.panel-grid .g-start-sm-17{grid-column-start:17}.panel-grid .g-start-sm-18{grid-column-start:18}.panel-grid .g-start-sm-19{grid-column-start:19}.panel-grid .g-start-sm-20{grid-column-start:20}.panel-grid .g-start-sm-21{grid-column-start:21}.panel-grid .g-start-sm-22{grid-column-start:22}.panel-grid .g-start-sm-23{grid-column-start:23}}@media(min-width: 768px){.panel-grid .g-col-md-1{grid-column:auto/span 1}.panel-grid .g-col-md-2{grid-column:auto/span 2}.panel-grid .g-col-md-3{grid-column:auto/span 3}.panel-grid .g-col-md-4{grid-column:auto/span 4}.panel-grid .g-col-md-5{grid-column:auto/span 5}.panel-grid .g-col-md-6{grid-column:auto/span 6}.panel-grid .g-col-md-7{grid-column:auto/span 7}.panel-grid .g-col-md-8{grid-column:auto/span 8}.panel-grid .g-col-md-9{grid-column:auto/span 9}.panel-grid .g-col-md-10{grid-column:auto/span 10}.panel-grid .g-col-md-11{grid-column:auto/span 11}.panel-grid .g-col-md-12{grid-column:auto/span 12}.panel-grid .g-col-md-13{grid-column:auto/span 13}.panel-grid .g-col-md-14{grid-column:auto/span 14}.panel-grid .g-col-md-15{grid-column:auto/span 15}.panel-grid .g-col-md-16{grid-column:auto/span 16}.panel-grid .g-col-md-17{grid-column:auto/span 17}.panel-grid .g-col-md-18{grid-column:auto/span 18}.panel-grid .g-col-md-19{grid-column:auto/span 19}.panel-grid .g-col-md-20{grid-column:auto/span 20}.panel-grid .g-col-md-21{grid-column:auto/span 21}.panel-grid .g-col-md-22{grid-column:auto/span 22}.panel-grid .g-col-md-23{grid-column:auto/span 23}.panel-grid .g-col-md-24{grid-column:auto/span 24}.panel-grid .g-start-md-1{grid-column-start:1}.panel-grid .g-start-md-2{grid-column-start:2}.panel-grid .g-start-md-3{grid-column-start:3}.panel-grid .g-start-md-4{grid-column-start:4}.panel-grid .g-start-md-5{grid-column-start:5}.panel-grid .g-start-md-6{grid-column-start:6}.panel-grid .g-start-md-7{grid-column-start:7}.panel-grid .g-start-md-8{grid-column-start:8}.panel-grid .g-start-md-9{grid-column-start:9}.panel-grid .g-start-md-10{grid-column-start:10}.panel-grid .g-start-md-11{grid-column-start:11}.panel-grid .g-start-md-12{grid-column-start:12}.panel-grid .g-start-md-13{grid-column-start:13}.panel-grid .g-start-md-14{grid-column-start:14}.panel-grid .g-start-md-15{grid-column-start:15}.panel-grid .g-start-md-16{grid-column-start:16}.panel-grid .g-start-md-17{grid-column-start:17}.panel-grid .g-start-md-18{grid-column-start:18}.panel-grid .g-start-md-19{grid-column-start:19}.panel-grid .g-start-md-20{grid-column-start:20}.panel-grid .g-start-md-21{grid-column-start:21}.panel-grid .g-start-md-22{grid-column-start:22}.panel-grid .g-start-md-23{grid-column-start:23}}@media(min-width: 992px){.panel-grid .g-col-lg-1{grid-column:auto/span 1}.panel-grid .g-col-lg-2{grid-column:auto/span 2}.panel-grid .g-col-lg-3{grid-column:auto/span 3}.panel-grid .g-col-lg-4{grid-column:auto/span 4}.panel-grid .g-col-lg-5{grid-column:auto/span 5}.panel-grid .g-col-lg-6{grid-column:auto/span 6}.panel-grid .g-col-lg-7{grid-column:auto/span 7}.panel-grid .g-col-lg-8{grid-column:auto/span 8}.panel-grid .g-col-lg-9{grid-column:auto/span 9}.panel-grid .g-col-lg-10{grid-column:auto/span 10}.panel-grid .g-col-lg-11{grid-column:auto/span 11}.panel-grid .g-col-lg-12{grid-column:auto/span 12}.panel-grid .g-col-lg-13{grid-column:auto/span 13}.panel-grid .g-col-lg-14{grid-column:auto/span 14}.panel-grid .g-col-lg-15{grid-column:auto/span 15}.panel-grid .g-col-lg-16{grid-column:auto/span 16}.panel-grid .g-col-lg-17{grid-column:auto/span 17}.panel-grid .g-col-lg-18{grid-column:auto/span 18}.panel-grid .g-col-lg-19{grid-column:auto/span 19}.panel-grid .g-col-lg-20{grid-column:auto/span 20}.panel-grid .g-col-lg-21{grid-column:auto/span 21}.panel-grid .g-col-lg-22{grid-column:auto/span 22}.panel-grid .g-col-lg-23{grid-column:auto/span 23}.panel-grid .g-col-lg-24{grid-column:auto/span 24}.panel-grid .g-start-lg-1{grid-column-start:1}.panel-grid .g-start-lg-2{grid-column-start:2}.panel-grid .g-start-lg-3{grid-column-start:3}.panel-grid .g-start-lg-4{grid-column-start:4}.panel-grid .g-start-lg-5{grid-column-start:5}.panel-grid .g-start-lg-6{grid-column-start:6}.panel-grid .g-start-lg-7{grid-column-start:7}.panel-grid .g-start-lg-8{grid-column-start:8}.panel-grid .g-start-lg-9{grid-column-start:9}.panel-grid .g-start-lg-10{grid-column-start:10}.panel-grid .g-start-lg-11{grid-column-start:11}.panel-grid .g-start-lg-12{grid-column-start:12}.panel-grid .g-start-lg-13{grid-column-start:13}.panel-grid .g-start-lg-14{grid-column-start:14}.panel-grid .g-start-lg-15{grid-column-start:15}.panel-grid .g-start-lg-16{grid-column-start:16}.panel-grid .g-start-lg-17{grid-column-start:17}.panel-grid .g-start-lg-18{grid-column-start:18}.panel-grid .g-start-lg-19{grid-column-start:19}.panel-grid .g-start-lg-20{grid-column-start:20}.panel-grid .g-start-lg-21{grid-column-start:21}.panel-grid .g-start-lg-22{grid-column-start:22}.panel-grid .g-start-lg-23{grid-column-start:23}}@media(min-width: 1200px){.panel-grid .g-col-xl-1{grid-column:auto/span 1}.panel-grid .g-col-xl-2{grid-column:auto/span 2}.panel-grid .g-col-xl-3{grid-column:auto/span 3}.panel-grid .g-col-xl-4{grid-column:auto/span 4}.panel-grid .g-col-xl-5{grid-column:auto/span 5}.panel-grid .g-col-xl-6{grid-column:auto/span 6}.panel-grid .g-col-xl-7{grid-column:auto/span 7}.panel-grid .g-col-xl-8{grid-column:auto/span 8}.panel-grid .g-col-xl-9{grid-column:auto/span 9}.panel-grid .g-col-xl-10{grid-column:auto/span 10}.panel-grid .g-col-xl-11{grid-column:auto/span 11}.panel-grid .g-col-xl-12{grid-column:auto/span 12}.panel-grid .g-col-xl-13{grid-column:auto/span 13}.panel-grid .g-col-xl-14{grid-column:auto/span 14}.panel-grid .g-col-xl-15{grid-column:auto/span 15}.panel-grid .g-col-xl-16{grid-column:auto/span 16}.panel-grid .g-col-xl-17{grid-column:auto/span 17}.panel-grid .g-col-xl-18{grid-column:auto/span 18}.panel-grid .g-col-xl-19{grid-column:auto/span 19}.panel-grid .g-col-xl-20{grid-column:auto/span 20}.panel-grid .g-col-xl-21{grid-column:auto/span 21}.panel-grid .g-col-xl-22{grid-column:auto/span 22}.panel-grid .g-col-xl-23{grid-column:auto/span 23}.panel-grid .g-col-xl-24{grid-column:auto/span 24}.panel-grid .g-start-xl-1{grid-column-start:1}.panel-grid .g-start-xl-2{grid-column-start:2}.panel-grid .g-start-xl-3{grid-column-start:3}.panel-grid .g-start-xl-4{grid-column-start:4}.panel-grid .g-start-xl-5{grid-column-start:5}.panel-grid .g-start-xl-6{grid-column-start:6}.panel-grid .g-start-xl-7{grid-column-start:7}.panel-grid .g-start-xl-8{grid-column-start:8}.panel-grid .g-start-xl-9{grid-column-start:9}.panel-grid .g-start-xl-10{grid-column-start:10}.panel-grid .g-start-xl-11{grid-column-start:11}.panel-grid .g-start-xl-12{grid-column-start:12}.panel-grid .g-start-xl-13{grid-column-start:13}.panel-grid .g-start-xl-14{grid-column-start:14}.panel-grid .g-start-xl-15{grid-column-start:15}.panel-grid .g-start-xl-16{grid-column-start:16}.panel-grid .g-start-xl-17{grid-column-start:17}.panel-grid .g-start-xl-18{grid-column-start:18}.panel-grid .g-start-xl-19{grid-column-start:19}.panel-grid .g-start-xl-20{grid-column-start:20}.panel-grid .g-start-xl-21{grid-column-start:21}.panel-grid .g-start-xl-22{grid-column-start:22}.panel-grid .g-start-xl-23{grid-column-start:23}}@media(min-width: 1400px){.panel-grid .g-col-xxl-1{grid-column:auto/span 1}.panel-grid .g-col-xxl-2{grid-column:auto/span 2}.panel-grid .g-col-xxl-3{grid-column:auto/span 3}.panel-grid .g-col-xxl-4{grid-column:auto/span 4}.panel-grid .g-col-xxl-5{grid-column:auto/span 5}.panel-grid .g-col-xxl-6{grid-column:auto/span 6}.panel-grid .g-col-xxl-7{grid-column:auto/span 7}.panel-grid .g-col-xxl-8{grid-column:auto/span 8}.panel-grid .g-col-xxl-9{grid-column:auto/span 9}.panel-grid .g-col-xxl-10{grid-column:auto/span 10}.panel-grid .g-col-xxl-11{grid-column:auto/span 11}.panel-grid .g-col-xxl-12{grid-column:auto/span 12}.panel-grid .g-col-xxl-13{grid-column:auto/span 13}.panel-grid .g-col-xxl-14{grid-column:auto/span 14}.panel-grid .g-col-xxl-15{grid-column:auto/span 15}.panel-grid .g-col-xxl-16{grid-column:auto/span 16}.panel-grid .g-col-xxl-17{grid-column:auto/span 17}.panel-grid .g-col-xxl-18{grid-column:auto/span 18}.panel-grid .g-col-xxl-19{grid-column:auto/span 19}.panel-grid .g-col-xxl-20{grid-column:auto/span 20}.panel-grid .g-col-xxl-21{grid-column:auto/span 21}.panel-grid .g-col-xxl-22{grid-column:auto/span 22}.panel-grid .g-col-xxl-23{grid-column:auto/span 23}.panel-grid .g-col-xxl-24{grid-column:auto/span 24}.panel-grid .g-start-xxl-1{grid-column-start:1}.panel-grid .g-start-xxl-2{grid-column-start:2}.panel-grid .g-start-xxl-3{grid-column-start:3}.panel-grid .g-start-xxl-4{grid-column-start:4}.panel-grid .g-start-xxl-5{grid-column-start:5}.panel-grid .g-start-xxl-6{grid-column-start:6}.panel-grid .g-start-xxl-7{grid-column-start:7}.panel-grid .g-start-xxl-8{grid-column-start:8}.panel-grid .g-start-xxl-9{grid-column-start:9}.panel-grid .g-start-xxl-10{grid-column-start:10}.panel-grid .g-start-xxl-11{grid-column-start:11}.panel-grid .g-start-xxl-12{grid-column-start:12}.panel-grid .g-start-xxl-13{grid-column-start:13}.panel-grid .g-start-xxl-14{grid-column-start:14}.panel-grid .g-start-xxl-15{grid-column-start:15}.panel-grid .g-start-xxl-16{grid-column-start:16}.panel-grid .g-start-xxl-17{grid-column-start:17}.panel-grid .g-start-xxl-18{grid-column-start:18}.panel-grid .g-start-xxl-19{grid-column-start:19}.panel-grid .g-start-xxl-20{grid-column-start:20}.panel-grid .g-start-xxl-21{grid-column-start:21}.panel-grid .g-start-xxl-22{grid-column-start:22}.panel-grid .g-start-xxl-23{grid-column-start:23}}main{margin-top:1em;margin-bottom:1em}h1,.h1,h2,.h2{opacity:.9;margin-top:2rem;margin-bottom:1rem;font-weight:600}h1.title,.title.h1{margin-top:0}h2,.h2{border-bottom:1px solid #dee2e6;padding-bottom:.5rem}h3,.h3{font-weight:600}h3,.h3,h4,.h4{opacity:.9;margin-top:1.5rem}h5,.h5,h6,.h6{opacity:.9}.header-section-number{color:#747a7f}.nav-link.active .header-section-number{color:inherit}mark,.mark{padding:0em}.panel-caption,caption,.figure-caption{font-size:.9rem}.panel-caption,.figure-caption,figcaption{color:#747a7f}.table-caption,caption{color:#373a3c}.quarto-layout-cell[data-ref-parent] caption{color:#747a7f}.column-margin figcaption,.margin-caption,div.aside,aside,.column-margin{color:#747a7f;font-size:.825rem}.panel-caption.margin-caption{text-align:inherit}.column-margin.column-container p{margin-bottom:0}.column-margin.column-container>*:not(.collapse){padding-top:.5em;padding-bottom:.5em;display:block}.column-margin.column-container>*.collapse:not(.show){display:none}@media(min-width: 768px){.column-margin.column-container .callout-margin-content:first-child{margin-top:4.5em}.column-margin.column-container .callout-margin-content-simple:first-child{margin-top:3.5em}}.margin-caption>*{padding-top:.5em;padding-bottom:.5em}@media(max-width: 767.98px){.quarto-layout-row{flex-direction:column}}.nav-tabs .nav-item{margin-top:1px;cursor:pointer}.tab-content{margin-top:0px;border-left:#dee2e6 1px solid;border-right:#dee2e6 1px solid;border-bottom:#dee2e6 1px solid;margin-left:0;padding:1em;margin-bottom:1em}@media(max-width: 767.98px){.layout-sidebar{margin-left:0;margin-right:0}}.panel-sidebar,.panel-sidebar .form-control,.panel-input,.panel-input .form-control,.selectize-dropdown{font-size:.9rem}.panel-sidebar .form-control,.panel-input .form-control{padding-top:.1rem}.tab-pane div.sourceCode{margin-top:0px}.tab-pane>p{padding-top:1em}.tab-content>.tab-pane:not(.active){display:none !important}div.sourceCode{background-color:#282a36;border:1px solid #282a36;border-radius:.25rem}pre.sourceCode{background-color:rgba(0,0,0,0)}pre.sourceCode{border:none;font-size:.875em;overflow:visible !important;padding:.4em}.callout pre.sourceCode{padding-left:0}div.sourceCode{overflow-y:hidden}.callout div.sourceCode{margin-left:initial}.blockquote{font-size:inherit;padding-left:1rem;padding-right:1.5rem;color:#747a7f}.blockquote h1:first-child,.blockquote .h1:first-child,.blockquote h2:first-child,.blockquote .h2:first-child,.blockquote h3:first-child,.blockquote .h3:first-child,.blockquote h4:first-child,.blockquote .h4:first-child,.blockquote h5:first-child,.blockquote .h5:first-child{margin-top:0}pre{background-color:initial;padding:initial;border:initial}p code:not(.sourceCode),li code:not(.sourceCode),td code:not(.sourceCode){background-color:#f7f7f7;padding:.2em}nav p code:not(.sourceCode),nav li code:not(.sourceCode),nav td code:not(.sourceCode){background-color:rgba(0,0,0,0);padding:0}td code:not(.sourceCode){white-space:pre-wrap}#quarto-embedded-source-code-modal>.modal-dialog{max-width:1000px;padding-left:1.75rem;padding-right:1.75rem}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body{padding:0}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-body div.sourceCode{margin:0;padding:.2rem .2rem;border-radius:0px;border:none}#quarto-embedded-source-code-modal>.modal-dialog>.modal-content>.modal-header{padding:.7rem}.code-tools-button{font-size:1rem;padding:.15rem .15rem;margin-left:5px;color:#6c757d;background-color:rgba(0,0,0,0);transition:initial;cursor:pointer}.code-tools-button>.bi::before{display:inline-block;height:1rem;width:1rem;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:1rem 1rem}.code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button>.bi::before{background-image:url('data:image/svg+xml,')}#quarto-embedded-source-code-modal .code-copy-button-checked>.bi::before{background-image:url('data:image/svg+xml,')}.sidebar{will-change:top;transition:top 200ms linear;position:sticky;overflow-y:auto;padding-top:1.2em;max-height:100vh}.sidebar.toc-left,.sidebar.margin-sidebar{top:0px;padding-top:1em}.sidebar.toc-left>*,.sidebar.margin-sidebar>*{padding-top:.5em}.sidebar.quarto-banner-title-block-sidebar>*{padding-top:1.65em}figure .quarto-notebook-link{margin-top:.5em}.quarto-notebook-link{font-size:.75em;color:#6c757d;margin-bottom:1em;text-decoration:none;display:block}.quarto-notebook-link:hover{text-decoration:underline;color:#2780e3}.quarto-notebook-link::before{display:inline-block;height:.75rem;width:.75rem;margin-bottom:0em;margin-right:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}.quarto-alternate-notebooks i.bi,.quarto-alternate-formats i.bi{margin-right:.4em}.quarto-notebook .cell-container{display:flex}.quarto-notebook .cell-container .cell{flex-grow:4}.quarto-notebook .cell-container .cell-decorator{padding-top:1.5em;padding-right:1em;text-align:right}.quarto-notebook h2,.quarto-notebook .h2{border-bottom:none}.sidebar .quarto-alternate-formats a,.sidebar .quarto-alternate-notebooks a{text-decoration:none}.sidebar .quarto-alternate-formats a:hover,.sidebar .quarto-alternate-notebooks a:hover{color:#2780e3}.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2,.sidebar nav[role=doc-toc]>h2,.sidebar nav[role=doc-toc]>.h2{font-size:.875rem;font-weight:400;margin-bottom:.5rem;margin-top:.3rem;font-family:inherit;border-bottom:0;padding-bottom:0;padding-top:0px}.sidebar .quarto-alternate-notebooks h2,.sidebar .quarto-alternate-notebooks .h2,.sidebar .quarto-alternate-formats h2,.sidebar .quarto-alternate-formats .h2{margin-top:1rem}.sidebar nav[role=doc-toc]>ul a{border-left:1px solid #e9ecef;padding-left:.6rem}.sidebar .quarto-alternate-notebooks h2>ul a,.sidebar .quarto-alternate-notebooks .h2>ul a,.sidebar .quarto-alternate-formats h2>ul a,.sidebar .quarto-alternate-formats .h2>ul a{border-left:none;padding-left:.6rem}.sidebar .quarto-alternate-notebooks ul a:empty,.sidebar .quarto-alternate-formats ul a:empty,.sidebar nav[role=doc-toc]>ul a:empty{display:none}.sidebar .quarto-alternate-notebooks ul,.sidebar .quarto-alternate-formats ul,.sidebar nav[role=doc-toc] ul{padding-left:0;list-style:none;font-size:.875rem;font-weight:300}.sidebar .quarto-alternate-notebooks ul li a,.sidebar .quarto-alternate-formats ul li a,.sidebar nav[role=doc-toc]>ul li a{line-height:1.1rem;padding-bottom:.2rem;padding-top:.2rem;color:inherit}.sidebar nav[role=doc-toc] ul>li>ul>li>a{padding-left:1.2em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>a{padding-left:2.4em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>a{padding-left:3.6em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:4.8em}.sidebar nav[role=doc-toc] ul>li>ul>li>ul>li>ul>li>ul>li>ul>li>a{padding-left:6em}.sidebar nav[role=doc-toc] ul>li>a.active,.sidebar nav[role=doc-toc] ul>li>ul>li>a.active{border-left:1px solid #2780e3;color:#2780e3 !important}.sidebar nav[role=doc-toc] ul>li>a:hover,.sidebar nav[role=doc-toc] ul>li>ul>li>a:hover{color:#2780e3 !important}kbd,.kbd{color:#373a3c;background-color:#f8f9fa;border:1px solid;border-radius:5px;border-color:#dee2e6}div.hanging-indent{margin-left:1em;text-indent:-1em}.citation a,.footnote-ref{text-decoration:none}.footnotes ol{padding-left:1em}.tippy-content>*{margin-bottom:.7em}.tippy-content>*:last-child{margin-bottom:0}.table a{word-break:break-word}.table>thead{border-top-width:1px;border-top-color:#dee2e6;border-bottom:1px solid #b6babc}.callout{margin-top:1.25rem;margin-bottom:1.25rem;border-radius:.25rem;overflow-wrap:break-word}.callout .callout-title-container{overflow-wrap:anywhere}.callout.callout-style-simple{padding:.4em .7em;border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout.callout-style-default{border-left:5px solid;border-right:1px solid #dee2e6;border-top:1px solid #dee2e6;border-bottom:1px solid #dee2e6}.callout .callout-body-container{flex-grow:1}.callout.callout-style-simple .callout-body{font-size:.9rem;font-weight:400}.callout.callout-style-default .callout-body{font-size:.9rem;font-weight:400}.callout.callout-titled .callout-body{margin-top:.2em}.callout:not(.no-icon).callout-titled.callout-style-simple .callout-body{padding-left:1.6em}.callout.callout-titled>.callout-header{padding-top:.2em;margin-bottom:-0.2em}.callout.callout-style-simple>div.callout-header{border-bottom:none;font-size:.9rem;font-weight:600;opacity:75%}.callout.callout-style-default>div.callout-header{border-bottom:none;font-weight:600;opacity:85%;font-size:.9rem;padding-left:.5em;padding-right:.5em}.callout.callout-style-default div.callout-body{padding-left:.5em;padding-right:.5em}.callout.callout-style-default div.callout-body>:first-child{margin-top:.5em}.callout>div.callout-header[data-bs-toggle=collapse]{cursor:pointer}.callout.callout-style-default .callout-header[aria-expanded=false],.callout.callout-style-default .callout-header[aria-expanded=true]{padding-top:0px;margin-bottom:0px;align-items:center}.callout.callout-titled .callout-body>:last-child:not(.sourceCode),.callout.callout-titled .callout-body>div>:last-child:not(.sourceCode){margin-bottom:.5rem}.callout:not(.callout-titled) .callout-body>:first-child,.callout:not(.callout-titled) .callout-body>div>:first-child{margin-top:.25rem}.callout:not(.callout-titled) .callout-body>:last-child,.callout:not(.callout-titled) .callout-body>div>:last-child{margin-bottom:.2rem}.callout.callout-style-simple .callout-icon::before,.callout.callout-style-simple .callout-toggle::before{height:1rem;width:1rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.callout.callout-style-default .callout-icon::before,.callout.callout-style-default .callout-toggle::before{height:.9rem;width:.9rem;display:inline-block;content:"";background-repeat:no-repeat;background-size:.9rem .9rem}.callout.callout-style-default .callout-toggle::before{margin-top:5px}.callout .callout-btn-toggle .callout-toggle::before{transition:transform .2s linear}.callout .callout-header[aria-expanded=false] .callout-toggle::before{transform:rotate(-90deg)}.callout .callout-header[aria-expanded=true] .callout-toggle::before{transform:none}.callout.callout-style-simple:not(.no-icon) div.callout-icon-container{padding-top:.2em;padding-right:.55em}.callout.callout-style-default:not(.no-icon) div.callout-icon-container{padding-top:.1em;padding-right:.35em}.callout.callout-style-default:not(.no-icon) div.callout-title-container{margin-top:-1px}.callout.callout-style-default.callout-caution:not(.no-icon) div.callout-icon-container{padding-top:.3em;padding-right:.35em}.callout>.callout-body>.callout-icon-container>.no-icon,.callout>.callout-header>.callout-icon-container>.no-icon{display:none}div.callout.callout{border-left-color:#6c757d}div.callout.callout-style-default>.callout-header{background-color:#6c757d}div.callout-note.callout{border-left-color:#2780e3}div.callout-note.callout-style-default>.callout-header{background-color:#e9f2fc}div.callout-note:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-note .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-tip.callout{border-left-color:#3fb618}div.callout-tip.callout-style-default>.callout-header{background-color:#ecf8e8}div.callout-tip:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-tip .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-warning.callout{border-left-color:#ff7518}div.callout-warning.callout-style-default>.callout-header{background-color:#fff1e8}div.callout-warning:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-warning .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-caution.callout{border-left-color:#f0ad4e}div.callout-caution.callout-style-default>.callout-header{background-color:#fef7ed}div.callout-caution:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-caution .callout-toggle::before{background-image:url('data:image/svg+xml,')}div.callout-important.callout{border-left-color:#ff0039}div.callout-important.callout-style-default>.callout-header{background-color:#ffe6eb}div.callout-important:not(.callout-titled) .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important.callout-titled .callout-icon::before{background-image:url('data:image/svg+xml,');}div.callout-important .callout-toggle::before{background-image:url('data:image/svg+xml,')}.quarto-toggle-container{display:flex;align-items:center}.quarto-reader-toggle .bi::before,.quarto-color-scheme-toggle .bi::before{display:inline-block;height:1rem;width:1rem;content:"";background-repeat:no-repeat;background-size:1rem 1rem}.sidebar-navigation{padding-left:20px}.navbar .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.navbar .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle:not(.alternate) .bi::before{background-image:url('data:image/svg+xml,')}.sidebar-navigation .quarto-color-scheme-toggle.alternate .bi::before{background-image:url('data:image/svg+xml,')}.quarto-sidebar-toggle{border-color:#dee2e6;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem;border-style:solid;border-width:1px;overflow:hidden;border-top-width:0px;padding-top:0px !important}.quarto-sidebar-toggle-title{cursor:pointer;padding-bottom:2px;margin-left:.25em;text-align:center;font-weight:400;font-size:.775em}#quarto-content .quarto-sidebar-toggle{background:#fafafa}#quarto-content .quarto-sidebar-toggle-title{color:#373a3c}.quarto-sidebar-toggle-icon{color:#dee2e6;margin-right:.5em;float:right;transition:transform .2s ease}.quarto-sidebar-toggle-icon::before{padding-top:5px}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-icon{transform:rotate(-180deg)}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-title{border-bottom:solid #dee2e6 1px}.quarto-sidebar-toggle-contents{background-color:#fff;padding-right:10px;padding-left:10px;margin-top:0px !important;transition:max-height .5s ease}.quarto-sidebar-toggle.expanded .quarto-sidebar-toggle-contents{padding-top:1em;padding-bottom:10px}.quarto-sidebar-toggle:not(.expanded) .quarto-sidebar-toggle-contents{padding-top:0px !important;padding-bottom:0px}nav[role=doc-toc]{z-index:1020}#quarto-sidebar>*,nav[role=doc-toc]>*{transition:opacity .1s ease,border .1s ease}#quarto-sidebar.slow>*,nav[role=doc-toc].slow>*{transition:opacity .4s ease,border .4s ease}.quarto-color-scheme-toggle:not(.alternate).top-right .bi::before{background-image:url('data:image/svg+xml,')}.quarto-color-scheme-toggle.alternate.top-right .bi::before{background-image:url('data:image/svg+xml,')}#quarto-appendix.default{border-top:1px solid #dee2e6}#quarto-appendix.default{background-color:#fff;padding-top:1.5em;margin-top:2em;z-index:998}#quarto-appendix.default .quarto-appendix-heading{margin-top:0;line-height:1.4em;font-weight:600;opacity:.9;border-bottom:none;margin-bottom:0}#quarto-appendix.default .footnotes ol,#quarto-appendix.default .footnotes ol li>p:last-of-type,#quarto-appendix.default .quarto-appendix-contents>p:last-of-type{margin-bottom:0}#quarto-appendix.default .quarto-appendix-secondary-label{margin-bottom:.4em}#quarto-appendix.default .quarto-appendix-bibtex{font-size:.7em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-bibtex code.sourceCode{white-space:pre-wrap}#quarto-appendix.default .quarto-appendix-citeas{font-size:.9em;padding:1em;border:solid 1px #dee2e6;margin-bottom:1em}#quarto-appendix.default .quarto-appendix-heading{font-size:1em !important}#quarto-appendix.default *[role=doc-endnotes]>ol,#quarto-appendix.default .quarto-appendix-contents>*:not(h2):not(.h2){font-size:.9em}#quarto-appendix.default section{padding-bottom:1.5em}#quarto-appendix.default section *[role=doc-endnotes],#quarto-appendix.default section>*:not(a){opacity:.9;word-wrap:break-word}.btn.btn-quarto,div.cell-output-display .btn-quarto{color:#cbcccc;background-color:#373a3c;border-color:#373a3c}.btn.btn-quarto:hover,div.cell-output-display .btn-quarto:hover{color:#cbcccc;background-color:#555859;border-color:#4b4e50}.btn-check:focus+.btn.btn-quarto,.btn.btn-quarto:focus,.btn-check:focus+div.cell-output-display .btn-quarto,div.cell-output-display .btn-quarto:focus{color:#cbcccc;background-color:#555859;border-color:#4b4e50;box-shadow:0 0 0 .25rem rgba(77,80,82,.5)}.btn-check:checked+.btn.btn-quarto,.btn-check:active+.btn.btn-quarto,.btn.btn-quarto:active,.btn.btn-quarto.active,.show>.btn.btn-quarto.dropdown-toggle,.btn-check:checked+div.cell-output-display .btn-quarto,.btn-check:active+div.cell-output-display .btn-quarto,div.cell-output-display .btn-quarto:active,div.cell-output-display .btn-quarto.active,.show>div.cell-output-display .btn-quarto.dropdown-toggle{color:#fff;background-color:#5f6163;border-color:#4b4e50}.btn-check:checked+.btn.btn-quarto:focus,.btn-check:active+.btn.btn-quarto:focus,.btn.btn-quarto:active:focus,.btn.btn-quarto.active:focus,.show>.btn.btn-quarto.dropdown-toggle:focus,.btn-check:checked+div.cell-output-display .btn-quarto:focus,.btn-check:active+div.cell-output-display .btn-quarto:focus,div.cell-output-display .btn-quarto:active:focus,div.cell-output-display .btn-quarto.active:focus,.show>div.cell-output-display .btn-quarto.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(77,80,82,.5)}.btn.btn-quarto:disabled,.btn.btn-quarto.disabled,div.cell-output-display .btn-quarto:disabled,div.cell-output-display .btn-quarto.disabled{color:#fff;background-color:#373a3c;border-color:#373a3c}nav.quarto-secondary-nav.color-navbar{background-color:#f8f9fa;color:#545555}nav.quarto-secondary-nav.color-navbar h1,nav.quarto-secondary-nav.color-navbar .h1,nav.quarto-secondary-nav.color-navbar .quarto-btn-toggle{color:#545555}@media(max-width: 991.98px){body.nav-sidebar .quarto-title-banner{margin-bottom:0;padding-bottom:0}body.nav-sidebar #title-block-header{margin-block-end:0}}p.subtitle{margin-top:.25em;margin-bottom:.5em}code a:any-link{color:inherit;text-decoration-color:#6c757d}/*! light */div.observablehq table thead tr th{background-color:var(--bs-body-bg)}input,button,select,optgroup,textarea{background-color:var(--bs-body-bg)}.code-annotated .code-copy-button{margin-right:1.25em;margin-top:0;padding-bottom:0;padding-top:3px}.code-annotation-gutter-bg{background-color:#fff}.code-annotation-gutter{background-color:#282a36}.code-annotation-gutter,.code-annotation-gutter-bg{height:100%;width:calc(20px + .5em);position:absolute;top:0;right:0}dl.code-annotation-container-grid dt{margin-right:1em;margin-top:.25rem}dl.code-annotation-container-grid dt{font-family:var(--bs-font-monospace);color:#4f5457;border:solid #4f5457 1px;border-radius:50%;height:22px;width:22px;line-height:22px;font-size:11px;text-align:center;vertical-align:middle;text-decoration:none}dl.code-annotation-container-grid dt[data-target-cell]{cursor:pointer}dl.code-annotation-container-grid dt[data-target-cell].code-annotation-active{color:#fff;border:solid #aaa 1px;background-color:#aaa}pre.code-annotation-code{padding-top:0;padding-bottom:0}pre.code-annotation-code code{z-index:3}#code-annotation-line-highlight-gutter{width:100%;border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}#code-annotation-line-highlight{margin-left:-4em;width:calc(100% + 4em);border-top:solid rgba(170,170,170,.2666666667) 1px;border-bottom:solid rgba(170,170,170,.2666666667) 1px;z-index:2;background-color:rgba(170,170,170,.1333333333)}code.sourceCode .code-annotation-anchor.code-annotation-active{background-color:var(--quarto-hl-normal-color, #aaaaaa);border:solid var(--quarto-hl-normal-color, #aaaaaa) 1px;color:#282a36;font-weight:bolder}code.sourceCode .code-annotation-anchor{font-family:var(--bs-font-monospace);color:var(--quarto-hl-co-color);border:solid var(--quarto-hl-co-color) 1px;border-radius:50%;height:18px;width:18px;font-size:9px;margin-top:2px}code.sourceCode button.code-annotation-anchor{padding:2px}code.sourceCode a.code-annotation-anchor{line-height:18px;text-align:center;vertical-align:middle;cursor:default;text-decoration:none}@media print{.page-columns .column-screen-inset{grid-column:page-start-inset/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset table{background:#fff}.page-columns .column-screen-inset-left{grid-column:page-start-inset/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-left table{background:#fff}.page-columns .column-screen-inset-right{grid-column:body-content-start/page-end-inset;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-inset-right table{background:#fff}.page-columns .column-screen{grid-column:page-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen table{background:#fff}.page-columns .column-screen-left{grid-column:page-start/body-content-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-left table{background:#fff}.page-columns .column-screen-right{grid-column:body-content-start/page-end;z-index:998;transform:translate3d(0, 0, 0)}.page-columns .column-screen-right table{background:#fff}.page-columns .column-screen-inset-shaded{grid-column:page-start-inset/page-end-inset;padding:1em;background:#f8f9fa;z-index:998;transform:translate3d(0, 0, 0);margin-bottom:1em}}.quarto-video{margin-bottom:1em}.table>thead{border-top-width:0}.table>:not(caption)>*:not(:last-child)>*{border-bottom-color:#ebeced;border-bottom-style:solid;border-bottom-width:1px}.table>:not(:first-child){border-top:1px solid #b6babc;border-bottom:1px solid inherit}.table tbody{border-bottom-color:#b6babc}a.external:after{display:inline-block;height:.75rem;width:.75rem;margin-bottom:.15em;margin-left:.25em;content:"";vertical-align:-0.125em;background-image:url('data:image/svg+xml,');background-repeat:no-repeat;background-size:.75rem .75rem}div.sourceCode code a.external:after{content:none}a.external:after:hover{cursor:pointer}.quarto-ext-icon{display:inline-block;font-size:.75em;padding-left:.3em}.code-with-filename .code-with-filename-file{margin-bottom:0;padding-bottom:2px;padding-top:2px;padding-left:.7em;border:var(--quarto-border-width) solid var(--quarto-border-color);border-radius:var(--quarto-border-radius);border-bottom:0;border-bottom-left-radius:0%;border-bottom-right-radius:0%}.code-with-filename div.sourceCode,.reveal .code-with-filename div.sourceCode{margin-top:0;border-top-left-radius:0%;border-top-right-radius:0%}.code-with-filename .code-with-filename-file pre{margin-bottom:0}.code-with-filename .code-with-filename-file,.code-with-filename .code-with-filename-file pre{background-color:rgba(219,219,219,.8)}.quarto-dark .code-with-filename .code-with-filename-file,.quarto-dark .code-with-filename .code-with-filename-file pre{background-color:#555}.code-with-filename .code-with-filename-file strong{font-weight:400}.quarto-title-banner{margin-bottom:1em;color:#545555;background:#f8f9fa}.quarto-title-banner .code-tools-button{color:#878888}.quarto-title-banner .code-tools-button:hover{color:#545555}.quarto-title-banner .code-tools-button>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .code-tools-button:hover>.bi::before{background-image:url('data:image/svg+xml,')}.quarto-title-banner .quarto-title .title{font-weight:600}.quarto-title-banner .quarto-categories{margin-top:.75em}@media(min-width: 992px){.quarto-title-banner{padding-top:2.5em;padding-bottom:2.5em}}@media(max-width: 991.98px){.quarto-title-banner{padding-top:1em;padding-bottom:1em}}main.quarto-banner-title-block>section:first-child>h2,main.quarto-banner-title-block>section:first-child>.h2,main.quarto-banner-title-block>section:first-child>h3,main.quarto-banner-title-block>section:first-child>.h3,main.quarto-banner-title-block>section:first-child>h4,main.quarto-banner-title-block>section:first-child>.h4{margin-top:0}.quarto-title .quarto-categories{display:flex;flex-wrap:wrap;row-gap:.5em;column-gap:.4em;padding-bottom:.5em;margin-top:.75em}.quarto-title .quarto-categories .quarto-category{padding:.25em .75em;font-size:.65em;text-transform:uppercase;border:solid 1px;border-radius:.25rem;opacity:.6}.quarto-title .quarto-categories .quarto-category a{color:inherit}#title-block-header.quarto-title-block.default .quarto-title-meta{display:grid;grid-template-columns:repeat(2, 1fr)}#title-block-header.quarto-title-block.default .quarto-title .title{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-author-orcid img{margin-top:-5px}#title-block-header.quarto-title-block.default .quarto-description p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p,#title-block-header.quarto-title-block.default .quarto-title-authors p,#title-block-header.quarto-title-block.default .quarto-title-affiliations p{margin-bottom:.1em}#title-block-header.quarto-title-block.default .quarto-title-meta-heading{text-transform:uppercase;margin-top:1em;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-contents{font-size:.9em}#title-block-header.quarto-title-block.default .quarto-title-meta-contents a{color:#373a3c}#title-block-header.quarto-title-block.default .quarto-title-meta-contents p.affiliation:last-of-type{margin-bottom:.7em}#title-block-header.quarto-title-block.default p.affiliation{margin-bottom:.1em}#title-block-header.quarto-title-block.default .description,#title-block-header.quarto-title-block.default .abstract{margin-top:0}#title-block-header.quarto-title-block.default .description>p,#title-block-header.quarto-title-block.default .abstract>p{font-size:.9em}#title-block-header.quarto-title-block.default .description>p:last-of-type,#title-block-header.quarto-title-block.default .abstract>p:last-of-type{margin-bottom:0}#title-block-header.quarto-title-block.default .description .abstract-title,#title-block-header.quarto-title-block.default .abstract .abstract-title{margin-top:1em;text-transform:uppercase;font-size:.8em;opacity:.8;font-weight:400}#title-block-header.quarto-title-block.default .quarto-title-meta-author{display:grid;grid-template-columns:1fr 1fr}.quarto-title-tools-only{display:flex;justify-content:right}body{-webkit-font-smoothing:antialiased}.badge.bg-light{color:#373a3c}.progress .progress-bar{font-size:8px;line-height:8px}/*# sourceMappingURL=892720f5b23b427b262f377d412b1031.css.map */ diff --git a/exercises/multiple_linear_regression.ipynb b/exercises/multiple_linear_regression.ipynb index e2cb5ed..16ad462 100644 --- a/exercises/multiple_linear_regression.ipynb +++ b/exercises/multiple_linear_regression.ipynb @@ -346,7 +346,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "metadata": { "tags": [] }, @@ -364,13 +364,13 @@ "X_grid, Y_grid = np.meshgrid(x_vec, y_vec)\n", "\n", "# Create intercept grid\n", - "intercept = np.ones(X_grid.shape)\n", + "intercept_grid = np.ones(X_grid.shape)\n", "\n", "# Get parameter values\n", "pars = results_prunned.params\n", "\n", "# Z is the elevation of this 2D grid\n", - "Z_grid = intercept*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]\n" + "Z_grid = intercept_grid*pars[0] + X_grid*pars[1] + X_grid*Y_grid*pars[2]\n" ] }, { @@ -388,7 +388,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, "metadata": { "tags": [] }, @@ -428,7 +428,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 22, "metadata": { "tags": [] }, @@ -440,7 +440,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 23, "metadata": { "tags": [] },