From aa02e35e466610b8239b8bb00f1c1094663a120c Mon Sep 17 00:00:00 2001
From: Heather Savoy <47045484+HeatherSavoy-USDA@users.noreply.github.com>
Date: Thu, 5 Oct 2023 10:16:56 -0600
Subject: [PATCH 1/7] copy over annual workshop tutorial on spatial modeling
with ML
---
tutorials/GRWG23_spatial_modeling_ml.ipynb | 656 +++++++++++++++++++++
1 file changed, 656 insertions(+)
create mode 100644 tutorials/GRWG23_spatial_modeling_ml.ipynb
diff --git a/tutorials/GRWG23_spatial_modeling_ml.ipynb b/tutorials/GRWG23_spatial_modeling_ml.ipynb
new file mode 100644
index 0000000..0b3c348
--- /dev/null
+++ b/tutorials/GRWG23_spatial_modeling_ml.ipynb
@@ -0,0 +1,656 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "stone-albany",
+ "metadata": {},
+ "source": [
+ "# Spatial modeling with machine learning\n",
+ "\n",
+ "This tutorial will implement and compare machine learning techniques with two approaches to including spatial proximity for spatial modeling tasks:\n",
+ " * Spatial interpolation from point observations\n",
+ " * Spatial prediction from point observations and gridded covariates\n",
+ "\n",
+ "*Primary Libraries/Packages*:\n",
+ "\n",
+ "|Name|Description|Link|\n",
+ "|-|-|-|\n",
+ "| `pandas` | Dataframes and other datatypes for data analysis and manipulation | https://pandas.pydata.org/ |\n",
+ "| `geopandas` | Extends datatypes used by pandas to allow spatial operations on geometric types | https://geopandas.org/en/stable/ |\n",
+ "| `scikit-learn` | Machine Learning in Python | https://scikit-learn.org/stable/ |\n",
+ "| `plotnine` | A plotting library for Python modeled after R's [ggplot2](https://ggplot2.tidyverse.org/) | https://plotnine.readthedocs.io/en/v0.12.3/ |\n",
+ "\n",
+ "\n",
+ "*Terminology*:\n",
+ " * *(Spatial) Interpolation*: Using observations of dependent and\n",
+ " independent variables to estimate the value of the dependent\n",
+ " variable at unobserved independent variable values. For spatial\n",
+ " applications, this can be the case of having point observations\n",
+ " (i.e., variable observations at known x-y coordinates) and then\n",
+ " predicting a gridded map of the variable (i.e., estimating the\n",
+ " variable at the remaining x-y cells in the study area).\n",
+ " * *Random Forest*: A supervised machine learning algorithm that \n",
+ " uses an ensemble of decision trees for regression or \n",
+ " classification. \n",
+ "\n",
+ "*Tutorial Steps*:\n",
+ " * 1\\. **[Read in and visualize point observations](#step_1)**\n",
+ " * 2\\. **[Random Forest for spatial interpolation](#step_2)**\n",
+ " Use Random Forest to interpolate zinc concentrations across the study area in two ways:\n",
+ " * 2.a. *RFsp*: distance to all observations\n",
+ " * 2.b. *RFSI*: n observed values and distance to those n observation locations\n",
+ " * 3\\. **[Bringing in gridded covariates](#step_3)**"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "economic-orchestra",
+ "metadata": {},
+ "source": [
+ "## 0. Preliminary code\n",
+ "\n",
+ "First, we will import required packages and set a large default figure size. We will also define a function to print model metrics."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "answering-external",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from sklearn.ensemble import RandomForestRegressor\n",
+ "from sklearn.metrics import r2_score, mean_squared_error\n",
+ "from sklearn.model_selection import train_test_split, RandomizedSearchCV\n",
+ "from scipy.stats import randint\n",
+ "import geopandas as gpd\n",
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "import plotnine as pn\n",
+ "\n",
+ "pn.options.figure_size = (10, 10)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2ce5d3d7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def print_metrics(y_test, y_pred):\n",
+ " \"\"\"\n",
+ " Given observed and predicted y values, print the R^2 and RMSE metrics.\n",
+ "\n",
+ " y_test (Series): The observed y values.\n",
+ " y_pred (Series): The predicted y values.\n",
+ " \"\"\"\n",
+ " # R^2\n",
+ " r2 = r2_score(y_test, y_pred)\n",
+ " # Root mean squared error - RMSE\n",
+ " rmse = mean_squared_error(y_test, y_pred, squared = False)\n",
+ " print(\"R^2 = {0:3.2f}, RMSE = {1:5.0f}\".format(r2,rmse))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "necessary-marijuana",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## 1. Read in and visualize point observations\n",
+ "\n",
+ "We will open three vector datasets representing the point observations, the grid across which we want to predict, and the location of the river surrounding the study area. \n",
+ "\n",
+ "This dataset gives locations and topsoil heavy metal concentrations, along with a number of soil and landscape variables at the observation locations, collected in a flood plain of the river Meuse, near the village of Stein (NL). Heavy metal concentrations are from composite samples of an area of approximately 15 m x 15 m. The data were extracted from the [`sp` R package](https://cran.r-project.org/web/packages/sp/index.html)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "external-andorra",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Point observations and study area grid\n",
+ "meuse_obs = gpd.read_file('data/meuse_obs.shp')\n",
+ "meuse_grid = gpd.read_file('data/meuse_grid.shp')\n",
+ "\n",
+ "# Extra information for visualization:\n",
+ "xmin, ymin, xmax, ymax = meuse_grid.total_bounds\n",
+ "meuse_riv = gpd.read_file('data/meuse_riv.shp')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d05a36f4",
+ "metadata": {},
+ "source": [
+ "Let’s take a quick look at the dataset. Below is a map of the study area grid and the observation locations, plus the location of the river Meuse for reference. We can see that observed zinc concentrations tend to be higher closer to the river."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a1683625",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(pn.ggplot()\n",
+ " + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)\n",
+ " + pn.geom_map(meuse_grid, fill = None, size = 0.05)\n",
+ " + pn.geom_map(meuse_obs, pn.aes(fill = 'zinc'), color = None, size = 3)\n",
+ " + pn.scale_y_continuous(limits = [ymin, ymax]) \n",
+ " + pn.scale_fill_continuous(trans = 'log1p') \n",
+ " + pn.theme_minimal() \n",
+ " + pn.coord_fixed()\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "earned-broadway",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## 2. Random Forest for spatial interpolation\n",
+ "\n",
+ "We will explore two methods from recent literature that combine spatial proximity information as variables in fitting Random Forest models for spatial interpolation. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "3a6d5d39",
+ "metadata": {},
+ "source": [
+ "### 2.a. *RFsp*: distance to all observations\n",
+ "\n",
+ "First, we will implement the *RFsp* method from [Hengl et al 2018](https://doi.org/10.7717/peerj.5518). This method involves using the distance to every observation as a predictor. For example, if there are 10 observations of the target variable, then there would be 10 predictor variables with the ith predictor variable representing the distance to the ith observation. \n",
+ "\n",
+ "If you want to learn more about this approach, see the [thengl/GeoMLA](https://github.com/thengl/GeoMLA) GitHub repo or the [Spatial and spatiotemporal interpolation using Ensemble Machine Learning](https://opengeohub.github.io/spatial-prediction-eml/index.html) site from the same creators. Note: these resources are for R, but the latter does mention the `scikit-learn` library in Python that we will be using in this tutorial.\n",
+ "\n",
+ "To start, we will generate two DataFrames of distances:\n",
+ "* One with rows representing observations and columns representing observations (these will be our data for fitting and testing the model)\n",
+ "* One with rows representing grid cell centers and columns representing observations (these will be how we estimate maps of our target variable with the final model)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "surgical-parameter",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Get coordinates of grid cell centers - these are our locations at which we will\n",
+ "# want to interpolate our target variable.\n",
+ "grid_centers = meuse_grid.centroid\n",
+ "\n",
+ "# Generate a grid of distances to each observation\n",
+ "grid_distances = pd.DataFrame()\n",
+ "# We also need the distance values among our observations\n",
+ "obs_distances = pd.DataFrame()\n",
+ "\n",
+ "# We need a dataframe with rows representing prediction grid cells\n",
+ "# (or observations)\n",
+ "# and columns representing observations\n",
+ "for obs_index in range(meuse_obs.geometry.size):\n",
+ " cur_obs = meuse_obs.geometry.iloc[obs_index]\n",
+ " obs_name = 'obs_' + str(obs_index)\n",
+ " \n",
+ " cell_to_obs = grid_centers.distance(cur_obs).rename(obs_name)\n",
+ " grid_distances = pd.concat([grid_distances, cell_to_obs], axis=1)\n",
+ "\n",
+ " obs_to_obs = meuse_obs.distance(cur_obs).rename(obs_name)\n",
+ " obs_distances = pd.concat([obs_distances, obs_to_obs], axis=1)\n",
+ " "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a440ee18-6b7f-4bfb-b282-032c71d489c6",
+ "metadata": {},
+ "source": [
+ "Before moving on to model fitting, let's take a look at the distance matrices we created."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b114265f-0615-4bfd-bcbb-52f5fb71299c",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "markdown",
+ "id": "686dc861",
+ "metadata": {},
+ "source": [
+ "For fitting our model, we will use the distances among observations as our predictors and observed zinc concentration at those observations as our target variable."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "81c831c1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# matrix of distance to observations as predictors\n",
+ "RFsp_X = obs_distances\n",
+ "# vector of observed zinc concentration as target variable\n",
+ "y = meuse_obs['zinc']\n",
+ "\n",
+ "# We need to split our dataset into train and test datasets. We'll use 80% of\n",
+ "# the data for model training.\n",
+ "RFsp_X_train, RFsp_X_test, RFsp_y_train, RFsp_y_test = train_test_split(RFsp_X, y, train_size=0.8)\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a72d7324",
+ "metadata": {},
+ "source": [
+ "Machine learning algorithms typically have hyperparameters that can be tuned per application. Here, we will tune the number of trees in the random forest model and the maximum depth of the trees in the the random forest model. We use the training subset of our data for this fitting and tuning process. After the code chunk below, the best parameter values from our search are printed. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "17e27615",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "r_state = 0\n",
+ "\n",
+ "# Define the parameter space that will be searched over.\n",
+ "param_distributions = {'n_estimators': randint(1, 100),\n",
+ " 'max_depth': randint(5, 10)}\n",
+ "\n",
+ "# Now create a searchCV object and fit it to the data.\n",
+ "tuned_RFsp = RandomizedSearchCV(estimator=RandomForestRegressor(random_state=r_state),\n",
+ " n_iter=10,\n",
+ " param_distributions=param_distributions,\n",
+ " random_state=r_state).fit(RFsp_X_train, RFsp_y_train)\n",
+ "tuned_RFsp.best_params_"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "bf952281",
+ "metadata": {},
+ "source": [
+ "We can now use our testing subset of the data to quantify the model performance, i.e. how well did the model predict the remaining observed values? There are many potential metrics - see all the metrics `scikit-learn` supports [here](https://scikit-learn.org/stable/modules/model_evaluation.html#). The two we show below are the coefficient of determination ($R^2$) and the root mean square error ($RMSE$), two metrics that are likely familiar from outside machine learning as well."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8536f77b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_metrics(RFsp_y_test, tuned_RFsp.predict(RFsp_X_test))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "372f3e1a",
+ "metadata": {},
+ "source": [
+ "Our $R^2$ is not awesome! We typically want $R^2$ values closer to $1$ and RMSE values closer to $0$. Note: $RMSE$ is in the units of the target variable, so our zinc concentrations. You can see the range of values of zinc concentrations in the legend in the figure above, from which you can get a sense of our error. \n",
+ "\n",
+ "**Excercise:** Modify the `param_distributions` and `n_iter` values above - can you improve the metrics? Note that you may also increase the processing time."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2ab2eb71",
+ "metadata": {},
+ "source": [
+ "Once we are happy with (or at least curious about!) the model, we can predict and visualize our zinc concentration field."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "955a1c71",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Predict the value from all grid cells using their distances we determined above. \n",
+ "meuse_grid['pred_RFsp'] = tuned_RFsp.predict(grid_distances)\n",
+ "\n",
+ "(pn.ggplot()\n",
+ " + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)\n",
+ " + pn.geom_map(meuse_grid, pn.aes(fill = 'pred_RFsp'), color = 'white', size = 0.05)\n",
+ " + pn.geom_map(meuse_obs)\n",
+ " + pn.scale_y_continuous(limits = [ymin, ymax]) \n",
+ " + pn.scale_fill_distiller(type = 'div', palette = 'RdYlBu',trans = 'log1p') \n",
+ " + pn.theme_minimal() \n",
+ " + pn.coord_fixed()\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ca40657a",
+ "metadata": {},
+ "source": [
+ "### 2.b. *RFSI*: n observed values and distance to those n observation locations\n",
+ "\n",
+ "Now, we will try the the *RFSI* method from [Sekulić et al 2020](https://doi.org/10.3390/rs12101687). In this method, instead of using distances to *all* observations as our predictors, we will use distances to the _n_ closest observations as well as the observed values at those locations as our predictors.\n",
+ "\n",
+ "Below, we define a function to find the _n_ closest observations and record their distances and values."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4db5e98b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def nclosest_dist_value(dist_ij, obs_i, n = 3):\n",
+ " \"\"\"\n",
+ " Given a distance matrix among i locations and j observation \n",
+ " locations, j observed values, and the number of close\n",
+ " observations desired, generates a dataframe of distances to\n",
+ " and values at n closest observations for each of the i \n",
+ " locations.\n",
+ "\n",
+ " dist_ij (DataFrame): distance matrix among i locations and j \n",
+ " observation locations\n",
+ " obs_i (Series): The i observed values\n",
+ " n (int): The desired number of closest observations\n",
+ " \"\"\"\n",
+ " # Which observations are the n closest? \n",
+ " # But do not include distance to oneself. \n",
+ " # Note: ranks start at 1, not 0.\n",
+ " nclosest_dist_ij = dist_ij.replace(0.0,np.nan).rank(axis = 1, method = 'first') <= n\n",
+ " \n",
+ " nclosest = pd.DataFrame()\n",
+ "\n",
+ " # For each observation, find the n nearest observations and\n",
+ " # record the distance and target variable pairs\n",
+ " for i in range(dist_ij.shape[0]):\n",
+ " # Which obs are the n closest to the ith location?\n",
+ " nclosest_j_indices = np.where(nclosest_dist_ij.iloc[i,:])\n",
+ "\n",
+ " # Save the distance to and observed value at the n closest\n",
+ " # observations from the ith location\n",
+ " i_loc_dist = dist_ij.iloc[i].iloc[nclosest_j_indices]\n",
+ " sort_indices = i_loc_dist.values.argsort()\n",
+ " i_loc_dist = i_loc_dist.iloc[sort_indices]\n",
+ " i_loc_dist.rename(lambda x: 'dist' + str(np.where(x == i_loc_dist.index)[0][0]), inplace=True)\n",
+ " \n",
+ " i_loc_value = obs_i.iloc[nclosest_j_indices]\n",
+ " i_loc_value = i_loc_value.iloc[sort_indices]\n",
+ " i_loc_value.rename(lambda x: 'obs' + str(np.where(x == i_loc_value.index)[0][0]), inplace=True)\n",
+ " i_loc = pd.concat([i_loc_dist,i_loc_value],axis = 0)\n",
+ " nclosest = pd.concat([nclosest, pd.DataFrame(i_loc).transpose()], axis = 0)\n",
+ "\n",
+ " return nclosest"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f70cbd7a",
+ "metadata": {},
+ "source": [
+ "Let's now use that function to find and describe the n closest observations to each obsersevation and each grid cell. Note that we are taking advantage of the `obs_distances` and `grid_distances` variables we created for the *RFsp* approach. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f1b67e6c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "n = 10\n",
+ "obs_nclosest_obs = nclosest_dist_value(obs_distances, meuse_obs['zinc'], n)\n",
+ "grid_nclosest_obs = nclosest_dist_value(grid_distances, meuse_obs['zinc'], n)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "87928652-ddf4-40f5-a100-1e32473d8f7e",
+ "metadata": {},
+ "source": [
+ "Let's take a closer look at our new distance matrices."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ee343194-b3f5-45ee-b54f-0204fc001ffc",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9c267736",
+ "metadata": {},
+ "source": [
+ "We will then use the same model fitting process as for *RFsp*."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2d0baebe",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# matrix of distances to and observed values at the n closest observations as predictors\n",
+ "RFSI_X = obs_nclosest_obs\n",
+ "\n",
+ "# We need to split our dataset into train and test datasets. We'll use 80% of\n",
+ "# the data for model training.\n",
+ "RFSI_X_train, RFSI_X_test, RFSI_y_train, RFSI_y_test = train_test_split(RFSI_X, y, train_size=0.8)\n",
+ "\n",
+ "param_distributions = {'n_estimators': randint(1, 100),\n",
+ " 'max_depth': randint(5, 10)}\n",
+ "tuned_RFSI = RandomizedSearchCV(estimator=RandomForestRegressor(random_state=r_state),\n",
+ " n_iter=10,\n",
+ " param_distributions=param_distributions,\n",
+ " random_state=r_state).fit(RFSI_X_train, RFSI_y_train)\n",
+ "tuned_RFSI.best_params_\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a39806d6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_metrics(RFSI_y_test, tuned_RFSI.predict(RFSI_X_test))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c17a24b8",
+ "metadata": {},
+ "source": [
+ "**Exercise:** How does *RFSI*'s metrics compare to *RFsp*'s? What if you modify n, the number of closest observations? What if you modify the `param_distributions` and `n_iter` values like above?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9cbeabe4",
+ "metadata": {},
+ "source": [
+ "Let's visualize the two maps from these two methods together. To do so, we will need to transform our `meuse_grid` DataFrame into a longer format for plotting with facets in `plotnine`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6bb06848",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "meuse_grid['pred_RFSI'] = tuned_RFSI.predict(grid_nclosest_obs)\n",
+ "meuse_grid_long = pd.melt(meuse_grid, id_vars= 'geometry', value_vars=['pred_RFsp','pred_RFSI'])\n",
+ "\n",
+ "(pn.ggplot()\n",
+ " + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)\n",
+ " + pn.geom_map(meuse_grid_long, pn.aes(fill = 'value'), color = 'white', size = 0.05)\n",
+ " + pn.geom_map(meuse_obs)\n",
+ " + pn.scale_y_continuous(limits = [ymin, ymax]) \n",
+ " + pn.scale_fill_distiller(type = 'div', palette = 'RdYlBu',trans = 'log1p') \n",
+ " + pn.theme_minimal() \n",
+ " + pn.coord_fixed() \n",
+ " + pn.facet_wrap('variable')\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9148ff0b",
+ "metadata": {},
+ "source": [
+ "\n",
+ "## 3. Bringing in gridded covariates\n",
+ "\n",
+ "This dataset has three covariates supplied with the grid and the observations:\n",
+ "* dist: the distance to the river\n",
+ "* ffreq: a category describing the flooding frequency\n",
+ "* soil: a cateogory of soil type\n",
+ "\n",
+ "We can extend this spatial interpolation task into a more general spatial prediction task by including these co-located observations and gridded covariates. Let's visualize the flooding frequency: "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fe5a97e7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "(pn.ggplot()\n",
+ " + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)\n",
+ " + pn.geom_map(meuse_grid, pn.aes(fill = 'ffreq'), size = 0.05)\n",
+ " + pn.geom_map(meuse_obs, size = 2)\n",
+ " + pn.scale_y_continuous(limits = [ymin, ymax]) \n",
+ " + pn.theme_minimal() \n",
+ " + pn.coord_fixed()\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cc1ab6dd",
+ "metadata": {},
+ "source": [
+ "**Exercise**: Also visualize the other covariates. (Either one at a time, or try `melt`ing like above to use facets!). Do you expect these variables to improve the model?"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "293c9aa4",
+ "metadata": {},
+ "source": [
+ "Adding these covariates to the RF model, either method, is straightforward. We will stick to just the *RFSI* model here. All that needs to be done is to concatenate these three columns to our distance (and observed values) dataset and repeat the modeling fitting process."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c7bb455e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# matrix of distances to and observed values at the n closest observations as predictors\n",
+ "RFSI_wcov_X = pd.concat([obs_nclosest_obs.reset_index(),meuse_obs[['dist','ffreq','soil']]], axis=1)\n",
+ "\n",
+ "# We need to split our dataset into train and test datasets. We'll use 80% of\n",
+ "# the data for model training.\n",
+ "RFSI_wcov_X_train, RFSI_wcov_X_test, RFSI_wcov_y_train, RFSI_wcov_y_test = train_test_split(RFSI_wcov_X, y, train_size=0.8)\n",
+ "\n",
+ "param_distributions = {'n_estimators': randint(1, 100),\n",
+ " 'max_depth': randint(5, 10)}\n",
+ "tuned_RFSI_wcov = RandomizedSearchCV(estimator=RandomForestRegressor(random_state=r_state),\n",
+ " n_iter=10,\n",
+ " param_distributions=param_distributions,\n",
+ " random_state=r_state).fit(RFSI_wcov_X_train, RFSI_wcov_y_train)\n",
+ "tuned_RFSI_wcov.best_params_"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8874dfc0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print_metrics(RFSI_wcov_y_test, tuned_RFSI_wcov.predict(RFSI_wcov_X_test))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b83ace15",
+ "metadata": {},
+ "source": [
+ "**Exercise:** How did the new covariates change our metrics? Was it as you'd expect? "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c2e0744f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "grid_nclosest_obs_wcov = pd.concat([grid_nclosest_obs.reset_index(),meuse_grid[['dist','ffreq','soil']]], axis=1)\n",
+ "meuse_grid['pred_RFSI_wcov'] = tuned_RFSI_wcov.predict(grid_nclosest_obs_wcov)\n",
+ "\n",
+ "meuse_grid_long = pd.melt(meuse_grid, id_vars= 'geometry', value_vars=['pred_RFsp','pred_RFSI','pred_RFSI_wcov'])\n",
+ "\n",
+ "\n",
+ "(pn.ggplot()\n",
+ " + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)\n",
+ " + pn.geom_map(meuse_grid_long, pn.aes(fill = 'value'), color = 'white', size = 0.05)\n",
+ " + pn.geom_map(meuse_obs, size = 0.25)\n",
+ " + pn.scale_y_continuous(limits = [ymin, ymax]) \n",
+ " + pn.scale_fill_distiller(type = 'div', palette = 'RdYlBu',trans = 'log1p') \n",
+ " + pn.theme_minimal() \n",
+ " + pn.coord_fixed() \n",
+ " + pn.facet_wrap('variable')\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "655ae3db",
+ "metadata": {},
+ "source": [
+ "**Exercise**: Use the covariates to create a `RFsp_wcov` model and add it to the figure. How do the metrics compare to the other results?"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "grwg_workshop",
+ "language": "python",
+ "name": "grwg_workshop"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.8"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
From 59dc28bcde4494b71c7d307dbc5dd13e166250cd Mon Sep 17 00:00:00 2001
From: Heather Savoy <47045484+HeatherSavoy-USDA@users.noreply.github.com>
Date: Thu, 4 Jan 2024 10:41:35 -0700
Subject: [PATCH 2/7] Reformat python spatial modeling notebook to match
template
---
tutorials/GRWG23_spatial_modeling_ml.ipynb | 68 +++++++++++++++-------
1 file changed, 48 insertions(+), 20 deletions(-)
diff --git a/tutorials/GRWG23_spatial_modeling_ml.ipynb b/tutorials/GRWG23_spatial_modeling_ml.ipynb
index 0b3c348..a03ceb8 100644
--- a/tutorials/GRWG23_spatial_modeling_ml.ipynb
+++ b/tutorials/GRWG23_spatial_modeling_ml.ipynb
@@ -1,16 +1,38 @@
{
"cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "\n",
+ "**Last Update:** 4 January 2024
\n",
+ "**Download Jupyter Notebook**: [GRWG23_spatial_modeling_ml.ipynb](https://geospatial.101workbook.org/tutorials/GRWG23_spatial_modeling_ml.ipynb)"
+ ]
+ },
{
"cell_type": "markdown",
"id": "stone-albany",
"metadata": {},
"source": [
- "# Spatial modeling with machine learning\n",
+ "## Overview\n",
"\n",
"This tutorial will implement and compare machine learning techniques with two approaches to including spatial proximity for spatial modeling tasks:\n",
" * Spatial interpolation from point observations\n",
" * Spatial prediction from point observations and gridded covariates\n",
"\n",
+ "*Language*: `Python`\n",
+ "\n",
"*Primary Libraries/Packages*:\n",
"\n",
"|Name|Description|Link|\n",
@@ -21,7 +43,8 @@
"| `plotnine` | A plotting library for Python modeled after R's [ggplot2](https://ggplot2.tidyverse.org/) | https://plotnine.readthedocs.io/en/v0.12.3/ |\n",
"\n",
"\n",
- "*Terminology*:\n",
+ "## Nomenclature\n",
+ "\n",
" * *(Spatial) Interpolation*: Using observations of dependent and\n",
" independent variables to estimate the value of the dependent\n",
" variable at unobserved independent variable values. For spatial\n",
@@ -33,7 +56,8 @@
" uses an ensemble of decision trees for regression or \n",
" classification. \n",
"\n",
- "*Tutorial Steps*:\n",
+ "## Tutorial Steps\n",
+ "\n",
" * 1\\. **[Read in and visualize point observations](#step_1)**\n",
" * 2\\. **[Random Forest for spatial interpolation](#step_2)**\n",
" Use Random Forest to interpolate zinc concentrations across the study area in two ways:\n",
@@ -47,7 +71,7 @@
"id": "economic-orchestra",
"metadata": {},
"source": [
- "## 0. Preliminary code\n",
+ "## Step 0: Load libraries and define function\n",
"\n",
"First, we will import required packages and set a large default figure size. We will also define a function to print model metrics."
]
@@ -98,7 +122,7 @@
"metadata": {},
"source": [
"\n",
- "## 1. Read in and visualize point observations\n",
+ "## Step 1: Read in and visualize point observations\n",
"\n",
"We will open three vector datasets representing the point observations, the grid across which we want to predict, and the location of the river surrounding the study area. \n",
"\n",
@@ -113,12 +137,12 @@
"outputs": [],
"source": [
"# Point observations and study area grid\n",
- "meuse_obs = gpd.read_file('data/meuse_obs.shp')\n",
- "meuse_grid = gpd.read_file('data/meuse_grid.shp')\n",
+ "meuse_obs = gpd.read_file('../SpatialModeling/assets/meuse_obs.shp')\n",
+ "meuse_grid = gpd.read_file('../SpatialModeling/assets/meuse_grid.shp')\n",
"\n",
"# Extra information for visualization:\n",
"xmin, ymin, xmax, ymax = meuse_grid.total_bounds\n",
- "meuse_riv = gpd.read_file('data/meuse_riv.shp')"
+ "meuse_riv = gpd.read_file('../SpatialModeling/assets/meuse_riv.shp')"
]
},
{
@@ -153,7 +177,7 @@
"metadata": {},
"source": [
"\n",
- "## 2. Random Forest for spatial interpolation\n",
+ "## Step 2: Random Forest for spatial interpolation\n",
"\n",
"We will explore two methods from recent literature that combine spatial proximity information as variables in fitting Random Forest models for spatial interpolation. "
]
@@ -163,7 +187,7 @@
"id": "3a6d5d39",
"metadata": {},
"source": [
- "### 2.a. *RFsp*: distance to all observations\n",
+ "### Step 2a: *RFsp*: distance to all observations\n",
"\n",
"First, we will implement the *RFsp* method from [Hengl et al 2018](https://doi.org/10.7717/peerj.5518). This method involves using the distance to every observation as a predictor. For example, if there are 10 observations of the target variable, then there would be 10 predictor variables with the ith predictor variable representing the distance to the ith observation. \n",
"\n",
@@ -219,7 +243,9 @@
"id": "b114265f-0615-4bfd-bcbb-52f5fb71299c",
"metadata": {},
"outputs": [],
- "source": []
+ "source": [
+ "obs_distances"
+ ]
},
{
"cell_type": "markdown",
@@ -337,7 +363,7 @@
"id": "ca40657a",
"metadata": {},
"source": [
- "### 2.b. *RFSI*: n observed values and distance to those n observation locations\n",
+ "### 2b: *RFSI*: n observed values and distance to those n observation locations\n",
"\n",
"Now, we will try the the *RFSI* method from [Sekulić et al 2020](https://doi.org/10.3390/rs12101687). In this method, instead of using distances to *all* observations as our predictors, we will use distances to the _n_ closest observations as well as the observed values at those locations as our predictors.\n",
"\n",
@@ -427,7 +453,9 @@
"id": "ee343194-b3f5-45ee-b54f-0204fc001ffc",
"metadata": {},
"outputs": [],
- "source": []
+ "source": [
+ "obs_nclosest_obs"
+ ]
},
{
"cell_type": "markdown",
@@ -475,7 +503,7 @@
"id": "c17a24b8",
"metadata": {},
"source": [
- "**Exercise:** How does *RFSI*'s metrics compare to *RFsp*'s? What if you modify n, the number of closest observations? What if you modify the `param_distributions` and `n_iter` values like above?"
+ "How does *RFSI*'s metrics compare to *RFsp*'s? What if you modify n, the number of closest observations? What if you modify the `param_distributions` and `n_iter` values like above?"
]
},
{
@@ -514,7 +542,7 @@
"metadata": {},
"source": [
"\n",
- "## 3. Bringing in gridded covariates\n",
+ "## Step 3: Bringing in gridded covariates\n",
"\n",
"This dataset has three covariates supplied with the grid and the observations:\n",
"* dist: the distance to the river\n",
@@ -595,7 +623,7 @@
"id": "b83ace15",
"metadata": {},
"source": [
- "**Exercise:** How did the new covariates change our metrics? Was it as you'd expect? "
+ "How did the new covariates change our metrics? Was it as you'd expect? "
]
},
{
@@ -628,15 +656,15 @@
"id": "655ae3db",
"metadata": {},
"source": [
- "**Exercise**: Use the covariates to create a `RFsp_wcov` model and add it to the figure. How do the metrics compare to the other results?"
+ "Use the covariates to create a `RFsp_wcov` model and add it to the figure. How do the metrics compare to the other results?"
]
}
],
"metadata": {
"kernelspec": {
- "display_name": "grwg_workshop",
+ "display_name": "workshop",
"language": "python",
- "name": "grwg_workshop"
+ "name": "python3"
},
"language_info": {
"codemirror_mode": {
@@ -648,7 +676,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.8"
+ "version": "3.9.13"
}
},
"nbformat": 4,
From aa008d50b7fd5eefbbc923bd11bb3b04b3c3fbfc Mon Sep 17 00:00:00 2001
From: Heather Savoy <47045484+HeatherSavoy-USDA@users.noreply.github.com>
Date: Thu, 4 Jan 2024 10:43:05 -0700
Subject: [PATCH 3/7] rename python spatial modeling notebook
---
...modeling_ml.ipynb => GRWG23_SpatialInterpolation_python.ipynb} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename tutorials/{GRWG23_spatial_modeling_ml.ipynb => GRWG23_SpatialInterpolation_python.ipynb} (100%)
diff --git a/tutorials/GRWG23_spatial_modeling_ml.ipynb b/tutorials/GRWG23_SpatialInterpolation_python.ipynb
similarity index 100%
rename from tutorials/GRWG23_spatial_modeling_ml.ipynb
rename to tutorials/GRWG23_SpatialInterpolation_python.ipynb
From 4e0ecdb88e0fbadef239e9bb4c0787a498ae156e Mon Sep 17 00:00:00 2001
From: Heather Savoy <47045484+HeatherSavoy-USDA@users.noreply.github.com>
Date: Thu, 4 Jan 2024 11:11:44 -0700
Subject: [PATCH 4/7] Add python spatial modeling pages and data
---
.../GRWG23_SpatialInterpolation_python.md | 1100 +++++++++++++++++
.../SpatialModeling-LandingPage.md | 2 +-
.../GRWG23_spatial_modeling_ml_22_0.png | Bin 0 -> 95689 bytes
.../GRWG23_spatial_modeling_ml_34_0.png | Bin 0 -> 134608 bytes
.../GRWG23_spatial_modeling_ml_36_0.png | Bin 0 -> 77707 bytes
.../GRWG23_spatial_modeling_ml_42_0.png | Bin 0 -> 132030 bytes
.../assets/GRWG23_spatial_modeling_ml_8_0.png | Bin 0 -> 46138 bytes
SpatialModeling/assets/meuse_grid.zip | Bin 0 -> 74079 bytes
SpatialModeling/assets/meuse_obs.zip | Bin 0 -> 7724 bytes
SpatialModeling/assets/meuse_riv.zip | Bin 0 -> 2424 bytes
.../GRWG23_SpatialInterpolation_python.ipynb | 7 +-
11 files changed, 1105 insertions(+), 4 deletions(-)
create mode 100644 SpatialModeling/GRWG23_SpatialInterpolation_python.md
create mode 100644 SpatialModeling/assets/GRWG23_spatial_modeling_ml_22_0.png
create mode 100644 SpatialModeling/assets/GRWG23_spatial_modeling_ml_34_0.png
create mode 100644 SpatialModeling/assets/GRWG23_spatial_modeling_ml_36_0.png
create mode 100644 SpatialModeling/assets/GRWG23_spatial_modeling_ml_42_0.png
create mode 100644 SpatialModeling/assets/GRWG23_spatial_modeling_ml_8_0.png
create mode 100644 SpatialModeling/assets/meuse_grid.zip
create mode 100644 SpatialModeling/assets/meuse_obs.zip
create mode 100644 SpatialModeling/assets/meuse_riv.zip
diff --git a/SpatialModeling/GRWG23_SpatialInterpolation_python.md b/SpatialModeling/GRWG23_SpatialInterpolation_python.md
new file mode 100644
index 0000000..89ac181
--- /dev/null
+++ b/SpatialModeling/GRWG23_SpatialInterpolation_python.md
@@ -0,0 +1,1100 @@
+---
+title: Spatial modeling with machine learning
+layout: single
+author: Heather Savoy
+author_profile: true
+header:
+ overlay_color: "444444"
+ overlay_image: /assets/images/margaret-weir-GZyjbLNOaFg-unsplash_dark.jpg
+---
+
+**Last Update:** 4 January 2024
+**Download Jupyter Notebook**: [GRWG23_spatial_modeling_ml.ipynb](https://geospatial.101workbook.org/tutorials/GRWG23_spatial_modeling_ml.ipynb)
+
+## Overview
+
+This tutorial will implement and compare machine learning techniques with two approaches to including spatial proximity for spatial modeling tasks:
+ * Spatial interpolation from point observations
+ * Spatial prediction from point observations and gridded covariates
+
+*Language*: `Python`
+
+*Primary Libraries/Packages*:
+
+|Name|Description|Link|
+|-|-|-|
+| `pandas` | Dataframes and other datatypes for data analysis and manipulation | https://pandas.pydata.org/ |
+| `geopandas` | Extends datatypes used by pandas to allow spatial operations on geometric types | https://geopandas.org/en/stable/ |
+| `scikit-learn` | Machine Learning in Python | https://scikit-learn.org/stable/ |
+| `plotnine` | A plotting library for Python modeled after R's [ggplot2](https://ggplot2.tidyverse.org/) | https://plotnine.readthedocs.io/en/v0.12.3/ |
+
+
+## Nomenclature
+
+ * *(Spatial) Interpolation*: Using observations of dependent and
+ independent variables to estimate the value of the dependent
+ variable at unobserved independent variable values. For spatial
+ applications, this can be the case of having point observations
+ (i.e., variable observations at known x-y coordinates) and then
+ predicting a gridded map of the variable (i.e., estimating the
+ variable at the remaining x-y cells in the study area).
+ * *Random Forest*: A supervised machine learning algorithm that
+ uses an ensemble of decision trees for regression or
+ classification.
+
+## Tutorial Steps
+
+ * 1\. **[Read in and visualize point observations](#step_1)**
+ * 2\. **[Random Forest for spatial interpolation](#step_2)**
+ Use Random Forest to interpolate zinc concentrations across the study area in two ways:
+ * 2.a. *RFsp*: distance to all observations
+ * 2.b. *RFSI*: n observed values and distance to those n observation locations
+ * 3\. **[Bringing in gridded covariates](#step_3)**
+
+## Step 0: Load libraries and define function
+
+First, we will import required packages and set a large default figure size. We will also define a function to print model metrics.
+
+
+```python
+from sklearn.ensemble import RandomForestRegressor
+from sklearn.metrics import r2_score, mean_squared_error
+from sklearn.model_selection import train_test_split, RandomizedSearchCV
+from scipy.stats import randint
+import geopandas as gpd
+import pandas as pd
+import numpy as np
+import plotnine as pn
+
+pn.options.figure_size = (10, 10)
+```
+
+
+```python
+def print_metrics(y_test, y_pred):
+ """
+ Given observed and predicted y values, print the R^2 and RMSE metrics.
+
+ y_test (Series): The observed y values.
+ y_pred (Series): The predicted y values.
+ """
+ # R^2
+ r2 = r2_score(y_test, y_pred)
+ # Root mean squared error - RMSE
+ rmse = mean_squared_error(y_test, y_pred, squared = False)
+ print("R^2 = {0:3.2f}, RMSE = {1:5.0f}".format(r2,rmse))
+```
+
+
+## Step 1: Read in and visualize point observations
+
+We will open three vector datasets representing the point observations, the grid across which we want to predict, and the location of the river surrounding the study area.
+
+This dataset gives locations and topsoil heavy metal concentrations, along with a number of soil and landscape variables at the observation locations, collected in a flood plain of the river Meuse, near the village of Stein (NL). Heavy metal concentrations are from composite samples of an area of approximately 15 m x 15 m. The data were extracted from the [`sp` R package](https://cran.r-project.org/web/packages/sp/index.html).
+
+
+```python
+dnld_url = 'https://geospatial.101workbook.org/SpatialModeling/assets/'
+# Point observations and study area grid
+meuse_obs = gpd.read_file(dnld_url + 'meuse_obs.zip')
+meuse_grid = gpd.read_file(dnld_url + 'meuse_grid.zip')
+
+# Extra information for visualization:
+xmin, ymin, xmax, ymax = meuse_grid.total_bounds
+meuse_riv = gpd.read_file(dnld_url + 'meuse_riv.zip')
+```
+
+Let’s take a quick look at the dataset. Below is a map of the study area grid and the observation locations, plus the location of the river Meuse for reference. We can see that observed zinc concentrations tend to be higher closer to the river.
+
+
+```python
+(pn.ggplot()
+ + pn.geom_map(meuse_riv, fill = '#1e90ff', color = None)
+ + pn.geom_map(meuse_grid, fill = None, size = 0.05)
+ + pn.geom_map(meuse_obs, pn.aes(fill = 'zinc'), color = None, size = 3)
+ + pn.scale_y_continuous(limits = [ymin, ymax])
+ + pn.scale_fill_continuous(trans = 'log1p')
+ + pn.theme_minimal()
+ + pn.coord_fixed()
+)
+```
+
+
+
+![png](assets/GRWG23_spatial_modeling_ml_8_0.png)
+
+
+
+
+## Step 2: Random Forest for spatial interpolation
+
+We will explore two methods from recent literature that combine spatial proximity information as variables in fitting Random Forest models for spatial interpolation.
+
+### Step 2a: *RFsp*: distance to all observations
+
+First, we will implement the *RFsp* method from [Hengl et al 2018](https://doi.org/10.7717/peerj.5518). This method involves using the distance to every observation as a predictor. For example, if there are 10 observations of the target variable, then there would be 10 predictor variables with the ith predictor variable representing the distance to the ith observation.
+
+If you want to learn more about this approach, see the [thengl/GeoMLA](https://github.com/thengl/GeoMLA) GitHub repo or the [Spatial and spatiotemporal interpolation using Ensemble Machine Learning](https://opengeohub.github.io/spatial-prediction-eml/index.html) site from the same creators. Note: these resources are for R, but the latter does mention the `scikit-learn` library in Python that we will be using in this tutorial.
+
+To start, we will generate two DataFrames of distances:
+* One with rows representing observations and columns representing observations (these will be our data for fitting and testing the model)
+* One with rows representing grid cell centers and columns representing observations (these will be how we estimate maps of our target variable with the final model)
+
+
+```python
+# Get coordinates of grid cell centers - these are our locations at which we will
+# want to interpolate our target variable.
+grid_centers = meuse_grid.centroid
+
+# Generate a grid of distances to each observation
+grid_distances = pd.DataFrame()
+# We also need the distance values among our observations
+obs_distances = pd.DataFrame()
+
+# We need a dataframe with rows representing prediction grid cells
+# (or observations)
+# and columns representing observations
+for obs_index in range(meuse_obs.geometry.size):
+ cur_obs = meuse_obs.geometry.iloc[obs_index]
+ obs_name = 'obs_' + str(obs_index)
+
+ cell_to_obs = grid_centers.distance(cur_obs).rename(obs_name)
+ grid_distances = pd.concat([grid_distances, cell_to_obs], axis=1)
+
+ obs_to_obs = meuse_obs.distance(cur_obs).rename(obs_name)
+ obs_distances = pd.concat([obs_distances, obs_to_obs], axis=1)
+
+```
+
+Before moving on to model fitting, let's take a look at the distance matrices we created.
+
+
+```python
+obs_distances
+```
+
+
+
+
+
+ | obs_0 | +obs_1 | +obs_2 | +obs_3 | +obs_4 | +obs_5 | +obs_6 | +obs_7 | +obs_8 | +obs_9 | +... | +obs_145 | +obs_146 | +obs_147 | +obs_148 | +obs_149 | +obs_150 | +obs_151 | +obs_152 | +obs_153 | +obs_154 | +
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | +0.000000 | +70.837843 | +118.848643 | +259.239272 | +366.314073 | +473.629602 | +258.321505 | +252.049598 | +380.189426 | +471.008492 | +... | +4304.014173 | +4385.565870 | +4425.191182 | +4194.974374 | +4077.205538 | +3914.407363 | +3868.323926 | +3964.443088 | +3607.233843 | +3449.821155 | +
1 | +70.837843 | +0.000000 | +141.566239 | +282.851551 | +362.640318 | +471.199533 | +234.401365 | +195.010256 | +328.867755 | +441.530293 | +... | +4236.122756 | +4316.784220 | +4355.550137 | +4126.296645 | +4007.817486 | +3845.342247 | +3798.730841 | +3894.291848 | +3538.899546 | +3391.434505 | +
2 | +118.848643 | +141.566239 | +0.000000 | +143.171226 | +251.023903 | +356.866922 | +167.000000 | +222.081066 | +323.513524 | +375.033332 | +... | +4278.052010 | +4365.122793 | +4411.447155 | +4173.908121 | +4061.434476 | +3896.201483 | +3854.403326 | +3956.156721 | +3584.262407 | +3389.963569 | +
3 | +259.239272 | +282.851551 | +143.171226 | +0.000000 | +154.262763 | +242.156974 | +175.171345 | +296.786118 | +347.351407 | +322.818835 | +... | +4292.750750 | +4386.465206 | +4440.764349 | +4194.687712 | +4088.695146 | +3920.739726 | +3884.100024 | +3992.349935 | +3603.447377 | +3361.647959 | +
4 | +366.314073 | +362.640318 | +251.023903 | +154.262763 | +0.000000 | +108.577162 | +147.526269 | +281.937936 | +266.101484 | +178.518907 | +... | +4162.607356 | +4260.340127 | +4319.896411 | +4068.314639 | +3966.640014 | +3796.976824 | +3763.871411 | +3876.723488 | +3476.475514 | +3212.786952 | +
... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +
150 | +3914.407363 | +3845.342247 | +3896.201483 | +3920.739726 | +3796.976824 | +3786.887904 | +3753.359162 | +3676.331051 | +3579.912988 | +3620.842443 | +... | +471.958685 | +476.656060 | +536.660041 | +296.082759 | +183.619171 | +0.000000 | +147.989865 | +334.846233 | +345.144897 | +1443.022176 | +
151 | +3868.323926 | +3798.730841 | +3854.403326 | +3884.100024 | +3763.871411 | +3757.931479 | +3714.900268 | +3633.511387 | +3540.952697 | +3589.008916 | +... | +599.736609 | +562.318415 | +557.046677 | +405.097519 | +217.082933 | +147.989865 | +0.000000 | +210.857772 | +391.256949 | +1545.369859 | +
152 | +3964.443088 | +3894.291848 | +3956.156721 | +3992.349935 | +3876.723488 | +3875.800046 | +3821.201513 | +3734.408655 | +3647.002194 | +3703.768081 | +... | +702.359595 | +596.896138 | +497.033198 | +494.814107 | +276.524863 | +334.846233 | +210.857772 | +0.000000 | +595.131078 | +1756.173397 | +
153 | +3607.233843 | +3538.899546 | +3584.262407 | +3603.447377 | +3476.475514 | +3462.718152 | +3438.127688 | +3365.864673 | +3265.476382 | +3299.412827 | +... | +702.659235 | +784.390209 | +880.273253 | +592.150319 | +528.674758 | +345.144897 | +391.256949 | +595.131078 | +0.000000 | +1176.606136 | +
154 | +3449.821155 | +3391.434505 | +3389.963569 | +3361.647959 | +3212.786952 | +3163.395170 | +3225.188987 | +3198.113350 | +3071.672183 | +3038.833493 | +... | +1461.677119 | +1666.870721 | +1877.419772 | +1521.862017 | +1600.647681 | +1443.022176 | +1545.369859 | +1756.173397 | +1176.606136 | +0.000000 | +
155 rows × 155 columns
++ | dist0 | +dist1 | +dist2 | +dist3 | +dist4 | +dist5 | +dist6 | +dist7 | +dist8 | +dist9 | +obs0 | +obs1 | +obs2 | +obs3 | +obs4 | +obs5 | +obs6 | +obs7 | +obs8 | +obs9 | +
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | +70.837843 | +118.848643 | +252.049598 | +258.321505 | +259.239272 | +336.434243 | +366.314073 | +373.483601 | +380.189426 | +399.656102 | +1141.0 | +640.0 | +406.0 | +346.0 | +257.0 | +1096.0 | +269.0 | +504.0 | +347.0 | +279.0 | +
0 | +70.837843 | +141.566239 | +195.010256 | +234.401365 | +266.011278 | +282.851551 | +311.081983 | +328.867755 | +356.349547 | +362.640318 | +1022.0 | +640.0 | +406.0 | +346.0 | +1096.0 | +257.0 | +504.0 | +347.0 | +279.0 | +269.0 | +
0 | +118.848643 | +141.566239 | +143.171226 | +167.000000 | +222.081066 | +251.023903 | +323.513524 | +326.401593 | +345.891602 | +351.973010 | +1022.0 | +1141.0 | +257.0 | +346.0 | +406.0 | +269.0 | +347.0 | +279.0 | +504.0 | +1096.0 | +
0 | +143.171226 | +154.262763 | +175.171345 | +242.156974 | +259.239272 | +282.851551 | +296.786118 | +322.818835 | +324.499615 | +347.351407 | +640.0 | +269.0 | +346.0 | +281.0 | +1022.0 | +1141.0 | +406.0 | +183.0 | +279.0 | +347.0 | +
0 | +108.577162 | +147.526269 | +154.262763 | +178.518907 | +221.758878 | +244.296950 | +251.023903 | +266.101484 | +281.937936 | +340.847473 | +281.0 | +346.0 | +257.0 | +183.0 | +279.0 | +189.0 | +640.0 | +347.0 | +406.0 | +326.0 | +
... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +... | +
0 | +117.038455 | +145.602198 | +147.989865 | +183.619171 | +217.117480 | +262.619497 | +269.818457 | +293.400750 | +296.082759 | +334.846233 | +224.0 | +189.0 | +496.0 | +214.0 | +187.0 | +400.0 | +296.0 | +154.0 | +258.0 | +342.0 | +
0 | +116.361506 | +141.000000 | +147.989865 | +160.863296 | +210.857772 | +217.082933 | +219.665655 | +243.772435 | +273.791892 | +287.702972 | +400.0 | +296.0 | +166.0 | +187.0 | +342.0 | +214.0 | +539.0 | +224.0 | +451.0 | +332.0 | +
0 | +81.412530 | +174.942848 | +205.000000 | +210.857772 | +242.866218 | +270.351623 | +276.524863 | +286.225436 | +296.278585 | +301.317109 | +332.0 | +400.0 | +420.0 | +496.0 | +296.0 | +539.0 | +214.0 | +553.0 | +451.0 | +577.0 | +
0 | +158.294030 | +170.390727 | +195.747286 | +260.555176 | +260.823695 | +309.161770 | +345.144897 | +353.747085 | +384.532183 | +385.020779 | +155.0 | +199.0 | +180.0 | +187.0 | +224.0 | +154.0 | +166.0 | +157.0 | +296.0 | +226.0 | +
0 | +353.004249 | +503.135171 | +799.335974 | +848.958185 | +870.917907 | +914.617406 | +986.887025 | +1013.126349 | +1038.171469 | +1043.985153 | +722.0 | +1672.0 | +192.0 | +130.0 | +203.0 | +157.0 | +778.0 | +113.0 | +240.0 | +199.0 | +
155 rows × 20 columns
+sUFAMEr!1!bfht_BH-C4wcm=|yL`qA4&wi%ak6nIp`r@OyU}67wdrc3p zJ cOuR6l{_(^01Hhh-44bsM|a)A|rQmuh`yRl|Ao-uUz#(N)9WCSY9z1xU^hw z>Ixv&Q6aO){ms(Ysu5`0>BP^(^%)T(=TgdhVtwk9+G^=BJ)nGy4%rwtv_CjJGY$~A z*st--6rT*R+r#ZYKS7k7ujtgF{v#^2-ufW$!#OMVdhZzG?R(Hh5tKTV6f@bn%kkG= z+UEEQz@jGXX)JxB_jHx2gTke4O9BUyRh(L8P!l0mk~T25#VK{v#kzsiTYT@CM9vs} zF-#$sZ|w+d;|> M0;XHv zB}44icVx92*$N>@<%}A(M+WcEkNsrlNLd+#1Ufa J}L$|-w zl8$y}8*~seYTDn?TRlXX>Q=Asd#{DJbH+V;B(gNcr%lmm YWD;MOnV$8-UM;}zAX3-;~Z82JUR-jrcI;nAt-?@8caOW_;1-2l;6G5VyLzN zx^$>nKQQ@v0xEM~DBgeUAUc^^U2_PB%W=MDbo_goAzJMB<}n+a_BTwJ@JjU)uM95O zlQdm2z%DdnaQFeRrG&<9x!(-SYU3v67sO5H9e0Nf2Ypp@)F%IYS8b|wnSZUJF-%!w z=p4{=R*kb&Gt1_`S33vw+Y8=%-SkBGbf;LP3B78sZ|ISJz1?t< >#Dpiypp|7lo@AOcItz0-dg}-*B+2W zX8YD+V!cO@qswRBt|ZoM6I*piYc5CYqvHh|2kARC4+;hy*zp(hoL|%xq<_h0htkqE zDnfhQYVjBOm+x!aowDpYOW=wHUx!Q{K#U{~>#35zlvLmKdS61A`|?HEkxDLHk%L3! z!^G98<~skii|@7^5m{{jmFGZnD4H;r05ree%*TD=-|V vke9S}(uMB3CJpy7( zN+6DPF@gGMTp`cNcWN_=>b9>OFV8zOO #7 8fGJokqEvo#py|@L~T^{YveL%;qM0XlbR%W;PpJRUH gUo>_6+y2qYx(xD&8DRo@YLBn-(!QqQQdO0FbHm0a6VEQ2pB!uEeSVC z1A|Rj+}E1Y{xj=&Un6t# |2s}f)xQ-KGO-a&z=}F-#Sq%94wIHiKKs5;!Fa9R9N#Ja_#LCVYMWiD zy1f)#aSi* U5EtOwWYtJV^JDewi`-?vP~=-1?p?zo*~1o||CU24Z*?h^=C= zacV}n-f|Q4Sv4WBLqED4Z%K~3SdFk=Be}d&^b4F}j@YGvJOXTxC03qZGRYHf$ce!4 z-#9P%O bd3f| gpaN#-{LAzC>2!B*&ahJ=m z^vm a*q{muSWgNd65RelMbHRT;)1WsDbxkyN-o^pzt}Kpfm&ip+zNEpLHQ@A z%9d-D2C3HN?xjVa=_h|&CiQ1In(~LpFmch4Eqj#PD`XHWMa9PS&@~g?`mlw|ioZ@b zq9^Z>yV;Ts3jzU-FgCzR{>aM#BQ>h;6b$nO+&U>+FRO^Qqn6%e!Il`lqxoQfZ#s&_ za2P4F-oh28@k7Q$LktY4mfkw&D!!GO5{UW5;o5E!azOq>qIKIz;QD}~w<=+${Dx65 zXphl05IA2MvRK|K;OD1~LAhrDb;mD*{smpyMI%0@=62>MOC_N%$6al%u0pQ%TsQak z3#+SB<*(K!MS_EY(Z<-7*7ZHSAqQK *OWvhtPKgOs1`p!vsoVg5a0|^;LM|O$+&XgrXM!%h$45Iu8J&@za+EhpJ%0e z;OG FMD0ilS4nB1Rudd`OfL`fqn>6*?N~*cN1>1c2IEC;xD!GB~Pi5w$_6OB`{N8pK z@_v4Xj~x&oRyCTm%75c#Pk$RV1sEi=BaIBEx=$4NB2(N M1~07^0quZIYs+r_F}=zNN4hgOQpjW=L(p~nQxo%l z(~b`kMDn^0l7sQPxep8g(83~S)6oZ-oU=WE 5CY326D7yLc z8wV}DkG#cl7gLrx@3R8bch&Tx+_M%YZt)DZTxIvj!WhP7n8I!h03IC9j(mh@uYH-D zK6x2FVZYKe*En!Q8^e1szJiumzz5<-n;iZqACvNLl=cI}4;yYl%X6r-Q{`Tb<%prB zuBp1&Eh{+N8xYh7zeD6h2hTTJV*$z+qx>AJWM3SkhW%(!e~FV%gbqg&!Gd #%CRZ7GSfv( z@9Eoq(bzHJ!3ocTp_HVDiECdv_DSdF2cj(+)eujvrsQ5yT66y1 ZW#9@;LXbQ(;7u4aynP0yqnWmp*`H!3X$fF<7b%>Z2(=1EP># zCU4+d2 fN zUXd;y4@4r otVIM{?YFtH^iz+oH`1{9X!-S|Eun%LoS z4Lb9aIj~1=1T3QG$y|7RcN(R^D f>H?$axCe|(A z%&tuQZXf5F#2xpvTN83o_+S44mt0hGo~OX1si# b7AsReE3wMg_8+(?hSd!g2EE8} zUFXe1lTb*V(&hTh@Jw~92c^lx_4{*KREsP>=@*_@^kMARE9ZFgz{1k4TV9{frPD?+ zcV9G#Y(l8aklTf~GU|mDRJ$`ZrVy^4KoxNA4nIPzByq8Y1ZyXv9MmWmFBa$NM)$Hi z=tjx;aK*zn3IIF*@pjEAishgsO~i-@xGyVoQ4L(0?40g&yl#+n5A+Nwvh#OZBjCRL zWfyD1^3N>r&W?uV;En2))D-9U+{co`(hFq7?DlmMJ2MXs&PPq#E#LI7v`CDHmHF{k zMmaoVNa()Pzq!c`3EbZTofOGepu?2kZ6<6>SlZQka@dX`T`&8Eq6k%Wjd5pau?ZJtg^arUKi z@I@-C$La{hQU&`y@>*tti8b(=_k3I<__>~jpey=HVk%^>tr}_scueXT6ricgU~%!u zJv&7atXFg-uL57Ezfz3*&~Vf1RU!cIgPj(tBKrj!{u?#@n>juGB_`%qB*xV 27W!EKM&3 z(r<<%ck537N%HTs mFiKr!5x6(v9;;^xYB6kY3%&UQ2f9K69RilJ9ISl7z9V_(Sy2C *B;3l%gAB?d%PF0m{=M)&n@ST+IyGF5RX&`eojT#pn-dvugr-q$nr_@kCpwb_% z2 +f#hYw%01V*O}jkD*TisTT1Kl5zZClnu8<@ zgwn#C)Ju^ >_chB-M- ;fAc)jvYk94B1Jc7HBZZk!Ez9ue*=X!GOK6 zZ!%O#oT>QX^g3K>!_|R3%huE{Yi2++an@rf1SBgm_Gh5Ugn#hqL 4l=fuY;ZPe5ZS_jdq@F2wU#*>Q9golF%9R`Eza!o z13?A00)Ym|39rG50sSrkGGVi1dDYH02X!TMk|zhy!(Uve;S%)B{@(5}1W0O}3 d*KEfS<*+YDJ}3J-$&{A~tXcSO4YNEgd;yW-wAiu#w^H99th`lw8;5 zGw~mFob*w3>I?JqYFEZG?IZQ+B>s73V$R>8&tTxpO^!R!ZQxA9ghxqcrIVn>U&$JQ zd$U*P%cS*Z*Y`HU;IeTeV`pO3ZT1@{16mMi*Q3Pn5q~kJrB7XLJrx!o{214!=&HuR zT($6S%~wE{MHkSi0d1^Ila+C!2T)RI8BthGz-h7f6}itXmzdQB4LenKmr_eLTz76^ zHW^}sTXwIdvGj*-GwVjuY?J*?kv}~9| zQDV_r6%{p^gzPo9j!-7DITqFTqSCgIa_M1dBs&nZuCrq>HeQ*x+yuF_w{!%*Sqnf7 z4d3WB)d8XW(SK3dv4CF!6bW47an&KlGFbps`-9;btlYtm?hm4*LgYnZCnAAw0NdlI zia9EG06G>=(4no^_WIOqgXs-&_#tJA)N4!z!r(mj?(X9Iqg?r=|LdBsU5@1cj+s5Y zUXXA5PlFPj7auajYkDg1`a7Gc)LFR9`41u-PD%5*2Lwei67eVp8lCYJeb-JMo?2>l zs)Sh&y1?)zcwtO(m#Ngo`4wCaCKz%EkWCR>?(6-gimO>LX0d5otE_IXaS=520k8*g ziI0xPEXXn*=>vfDTTQ!D(W(VQ)>NTtEb6CSqX_9TQTnV~YmDIrlaz=Y`n{O>r(aiU z$J6lWT|@f0xT)jD9C(l+NZRvt-`2U9twV_s9X*uEUTt@kdjLP3)I&I-X$Y-i)S8OW z)MwYJ{p9*5-837hNw# +yST&tIU=pkrQ+AMNt@> zhKz2?F^yjp26QQx@B#5&!KnN&}Z!;Rk`i{Q+?cJj>pAL_R*{1mYUyFH+%vt>nYV zkyO8-y$xn*=(Y{pTLM^-vlgUL;97}1; QoA0Y)Vq*wK_hXztUw*gl#5qbX-}EVeLD&c(2zgac63?l zV3nnKb>6FS(|NIBDqcn)fApQ=5BekgFhL}@eRlC^#y-cgTzdI)wC5qc;Fo+~;-c0~ z>$wfwREP;s05bO-Qzb+CuHgkGIGFU!b^ljfm+4WBeLGND`M5)Yy^^i1y CL zEQlUF_9F-Rui!|BKF;+R %TX~sQ_u0=vA^ZLFZEE- z7jBbCt4wG~K5 Lnb$Kpf(!#Omgb6Si zG>Y!AJ?&V-Y3`sXN=tO=?hcRxE3i@2dv WdqHJJ#p+Q1sm36~E4&eXgqt_|_`u(4 zTKt3mQRyfLTb!EPH@|B8jX{TQN#v0#V{$+qYlVCL_8?9&U+Lb?XV87h3^<1O<9oVB zG7qEYm-IgU5%BcxN%A@4?q_Jj={VaV@6=ar0mdJSiR=Cz(X~z7 8Km6->Sx)5%gqk EW z8l*#))pg9nIsB3kz(!*W!mMy_V_TbgdYB*ov;XI3b14$ZDuRtNyhZ0WKOn}WA;Zc} zwWQnx4TPRehF&hfyafMZ-9Mn_%o=9T|LnScwj)c?UFvES7XSQ(+h0liq 3#}UxL5#klcD9VK|sY2?M>*+2bt6+`BZ38)E&u1 zA)F+>_-3lsBbl9%=ixb&8Bd1P_80gl#U3D~m=Srhxs%-*ojyYcEqbjHi>5c8f71ju zu1%$q*GeYTOom~`M%88!dcHhNOEtkeKhEm;e4M6CPL$leIVruP>(+6MB--u@Mx!N} zK9Z!I@lqM7ZfACS5jf}wvot#N8k&hbZ#I#ilkzPVT0fUeLJObP0}&)9TDHQqBa?7c zITb^7e8m-CxSo8r^tqfmBs@pFOo|MEpkE`XNQVsh!gwboi$<5`Ev4X)i(!Krhlg3> z&g*|zL277Eu8@o!b+(#9#wDF$1}#u7o}-qMY=@DmQ Y6pi1PsC~@>UPB-drtqB0`BizQPMAb!H4zb!W8%yM#1U*IlphvANJJzr zvOjfs#}xmbYiZB~aQm8eP%sc_9*wbw2GnlO#ryc5|9Qx}wY_CMx;~hYl;nnZgr30o zMNHy3zO8{wl2&eR6||BxV4si>^k|sY;W~IxY41C%;rjaeQX&cPxH-(|D;}J)tlYHi zBatDG+@7KSL1-dyVMg6$mpq5v0b)Q!nlW&9vK> lm8CoT0{Yy>p>@?+;7>gM&*KT<)D=B$iM@(T2` }GP>^UF|)D-Z;or{3)6f8_kYVdJ!Jk`IK zmjOK?boD-S_D$2?A65&y^JEieIu~ `#mTe2lc$``Jxm+l+ z=Y`pSfTIhrNQz)@4sp7~@eGAAHVtY$rj%ztrh9ANJ}^%tRssVwrY{F&mz}>B=&PX< z*=()z&UBTvD&gOjSqtFHHfT~;q6aY<^F8me8ohD$UFkbQ0x!-=3S|Zl)*F7hlKm~e zheWoz09&+740i$kjiz?91+-QcPy-}RE$)hGh2`LZu7-1IOU*&6uU?*tEB?a7`=cQv z;1Dxh=A{FLuB7w)dke4^1(W-wza8rcx?6o*nFapDt^e)Y2xJ8{ Vr1hjzQshExCDb|J8wr>$r|?m@ z@oo%5#ypiZA&@xtADvn}3G&t5yxb?79o+G;X~pe_N>J6McEKKv{xCQ+8aN}`$0@&) zgcCgegOzwrSW>dS0l*bQ*vZ)$D}W&9XYMB@4Cu=z2yfsB`ph{mE8oe>8KRm=Ji}!L zEXqdNW7RY!O2rXt`%IQP`_V#PpO6T(e2HKRVhC3MSBq0r~3Jka-#g^i0rbo%9W> z+j4^7^7iV%Bo%A&qbcsmUbNqttr+w0QjN zn=Uy}=azqUpvh0J03QG5WGgyVV3^vRAU6Ls$f8}y7$`a-aslKovU%wezXg0Zy?M2T zz7sYb1aAf(+JBo3js8!AKS&P{FJ2FX0UdXh{>4^w8{))t$#HmI+L~(W9L!qLa^M^4 z89I4-c4m6Vi7W4)8|zR2C-#6BUvw!wk~uuH$|KI6kvye^-9=>r$4UdINK|g)eX&fd zY^DFnw$@?n&p(y2Bt%q^;(czg)2=Vz{zX;tsfpqm$^~_*O)GuH; z@?uHz=}@7?*Eb70VlYbpw3Qi71V2!`y}(0h96qu}vecdSGwdvq1hRs600 O2uS^U zDH)A0MqbdUrtvL}<~2t|vv|^R<$%#XPQbKQZ!)%TolD9LykM{9N8UE}s&8DTCQ7rd zFD}2TTWU^BE#5x7N!Qv2AOK#G23 r6DuxO^w)l7q z$>_Jfg0`jjJKs1VHr4;EA#^HtyLE)WotkaoJk9o~yG|zH#h^cz=b|xlm%JhqQx~Te zC`mIyk4ArVKig(M8CBIS@`$;D(C6LG0;fp-;j1!DXJ!5)CCl$BW1k84hjUvT13ozf zd6bpVr4%Opw3XHCDT;`8_})JC>x?+TeD+mPdh kJ4 zH5zbR{=N=SFXi5|U<=apdQ{5UmznmXekPAgeu+b1EKr?x55~~Jiaw*oB(|W~#MV*2 zh_G#=5%|Wn1~n%PF9p>^J?1@Ks-1P=Qs0@|=^?5KPzBtROpJUk)*kcAoGBrSp4*a6 z-ba+rP8^IP0}~$t1OC;JWKM8>3fg)2ni+cGxwzc7Xfh;=^me`XY`1051&@|8Wge5R z6xR62D? z-`N3SS4O;_ap%B^zm7iYN`psjNjZ0^@ZRlsE q)kk{1z?bWmOih z`7AI?S5Fg%>a%R*L~;mUSh=D*T~%e-^>4?l8Y) _w?LlbZyGm zZ(oI|+6u7^{!^@xOSqs@F_t+Nr}E3Q`QWvRLUw~2f3AOo-ZyRm%9I>TTrz+Mv`~bN z+HMCn6P)+kFXGnq%T$xj8Het*e|(LQA+Hr$Rj-o2dE!aqDnm`U@uKcuxW0Cme9)ZG zhg1BC_#X{)8@<)F-5*l$9mUeYo)scwLB@zJc^-ud`}#Ml8I+Et-#^oRv-2psZzS#n z*%v;zRIzQzj&FJQHNPYap8Xf(n)Is+k NZb8j}7gD!S^n#*D_Xq-f+(7v?+oi3Qd*g<(ky#&Tsqg)OfY$t*i|~(XW4X zNo^zpH3KPM_8`_nd5t+hHt64N` |E3@5x*WPjr^kOP?5?y z_p(9*)K7P@K86u95u@rPGpOEr=|UdR+QH~woe0EIOnT*-_$K4~K|KRc(w@bv6wJ@w z2P!oRFf8MKfRLuk87JU_7GUdwJoOu1P@sU~s16{-o5nxeE4+A_e+yr%m>A1*wj}J+ z-a@B>edghSZA*HMW8Po~%js)z=NXL89P=v&Lo^L&tACq3x#c)H*IxA9Szen7z>M zln!MJ`8(=;g^F&q9sj8_2VU6&d5xeC<~DNGZkjGN)%0k7JiMz6?xJJ6n#AXu&_@=a zOs`gH1J9tyLqs%FSM`vLWjfFN#FUcS iv)n zxw=2Z@r~u8$lsTfGQ;y7y9R&KkJ$_ey<<3~$-Qvb%z$q^%WQ1Ym|Rvy(*l`S-TpDp zP4B#c<%fC*ebPTQr89NW9I;b?^{p{X4EU(`{27UO+oD3UV!%aF%*J4r>x-KFw1@8v zCTmJXi?vLA`}wm^py|J-r+bq_P?o0#&1qC`WDfRo=sMPy2BdjpDx=gp#E`#sEu3Q6 z!jFzqQ^e+4tU3NmVO~nGSIAZe`*dq&Jv9$hlkF=C(vz*3sz*c|G zLvnwrqbJMurDS10fdkJHSTLUb2=1KXw^9p;Od-wM%g5(!$#}iz^ FMIE!FJT z#z*!1w*Q=rZFdteu65ds35RHBO*y@dnEYG`FQQt~$ys;*dcvSrE`gUNqX^_5uqY$Z zoO89XCAD+QPzk~3O6F%jubP}SP!!3mcnQcL1CziAJiobKuDV-zt9pg0Aj$x$(Dq4X zismmrLlT6bE8(oG&a7AeofUcl@w@H_wL9>qVMBKH%y2l)kblh&axP57bifL|I@x&Q zqY9YbOe`WiS|gz#_-oXhBu18ajby#KdLnSq`^n4u $T*5_ %lV|o+veq4;6GDQ{}XYYfV+;*M#-kG!$aMnUP6=kl>&(`sfU>f=J-aNm))TCw9 zC9}f9T_VolBKC7e>u3?moBFZ@vX`4fXDR-5zHjT>K1eYLndHvb_ehg7eMws?QzR^J z_NEdWE<-W9ce;(U>Wu5eOlhe$Yu8GXic5w`4+?+d{zmgzP0nx_dSFs1mgxDW%%;~9 zYAgJ=e{+5psP4sdqx3Lo0u^*;gMU2G=j~J%q~i>hS|Gu|eRn9T0lucAH><9e-nD3! zEVlNQyBu%>B0Bu4%gGD1j=H=Wl#XN#))`oHGrcwYfi{w+_`yT5MHPq43E_RY4R5`x zT-BCS1Km=?4tm?x6wxPO#s#Yle{IM(9{H(Pe(RN~3gADz!YK8e+cURwzOs|DTsF;| z@8ecQeCijwMka57LhP$cQ~g?t$H}rQ-}e^M0Dtd(^H(JRy9WCnlE9j5+ IRZt_gT;b zw7+h3Q#c%>C9fDi@p|E*U1`!~T #$KB^dUS9ug=xV%kg2oP tz;?5h|D;|#* z(D%HTaG n+uSSa<|r&oD`#uRa6T^<0@dSgcO;@OVroDenjIBb2pj_~ zZK9Z?ZJ#HB(_Ci!Eeajyktl=c(~Hj<1Bk=yuZEI^C4O^hkoF%bKx^>hjTUolQ+9 zotj*~EaZ#%^%MKCgcDP-H*a5hXC9bbj2} &^Cy$PyqZlx=j1njYsjzS?Y{CSEKzkX_FF(>&>ut0 zPTLqXw41{+fRbvp+Ms?LHb$){pPqJCSQ AxaO*N6?>QhuUrldP Qt}__y##bregtafo@5Y@dP@z|&@5mI$b^9&S2EPiG-x!@sXNcWxJzOFyeNz@ zYMvy!NK2eU()N8#WBkDy_sa-(Z`Z;5DdtdRdbRxZEPrRZMo-2Qcgs%n-{{4|{q#JQ z;(R=K$YCwS dfU@J?|)axDiMZ~XA~dILDS%-@Su zNV-zx$nPg_^<{Q@Sw~nlZfDGXZ);haY1QVYF=(_R2CC3kl4ftZZj5D{n9>WPMxLwb z)wqP$02WR=o=v*!bGc28jh3Z>=-y`mpk#)$krrqfv7!Nzkv13G-nQ-OHlEE5h_+-f z+4muri83SjqqBg2F3V&K=8YZz=2-z#17`yt4Fh@<{iRtvpXTA42*o?r&L0Myu2?Q! z2+blJ!+$wG&aHb3#G1bn%5O`ee*syaw}_5v=x#{M5&p#RVW#Jculg3_VUCr$^6P*v zxBI762H-CF$LO3+rOXET1f=f-+KJQ5xJ`22_>X%R6uWu=2e4=P8>WcQa7XQMw!^6K z7L}wQ3|+L+R^e@Sc-{7z-C8!d7ySoB0V;68t8ekfpIO7NJ$|avGGgV8WTt29g*_SN z&En!LC(zC``dQ#*{X~jB5>{^EwDm-(ZK{+n9_nsntf=~c9x002w@~1(8T~okLuk>+ zSHnlWG!C_SL`P3!M3u0`P%I4q7WL?3wyHCXt^W0@q5Uh2%cw!g75C=w$MB11jYmQo zbE+-qNJmp)FZf-JfSH#DEd@_6p3f3!tq=Cdq1zXas!feMdNuqcxpKi{5e_NN<7<*k zw83W^`i{2@bo%vj-P|FBymThy@s0ghV($tkb@wNK%Wq^+U!`{nfXL%f)w`Xf%pw|k z`1+FJQ*mDE;!g^klH|~|nXSp6H_i1 uIKBia(Re0$I`k zOjqeE(%TB%5IkdkQ$FU%-F?E#ti{$3)r;t$xX1_y2;~?YbWkE>)2^eL9?JXlVEYsj z9KbK=w^jsn1gt`jg4-b>mw8NqsGAkJ{G^Vho_cinK72#|=;0qe%kk|XgBA<={|3Qo zde0ukIjUJ5t $JG|BoLK&5SjQ bU0u{WZHJ|Q$W*g8Mq&|Iow8*{lgCEP=dLxYa|-7 zB)BLQf09Lh<_|M0^w4Q2sRmj&F2$ANf;d!Wb6@l)IxTkVEl9!2CQUm=G$a$LhQS6H z+f5pZIUEE3i7R^em4RlunklATXAFL;R8@o!p)d15oz9b{>ZTeJtylYc({4KeeF?DT zd^n_+rW(LvrcXeIOucKLj-MMI%Pf#<5n1)FX71e?#T`1z58e7F8FgrUAz%^nbb<7^ z)=!N##TRF0-=Z-t<#AOm&+-JJW#jA0??rS(mepze#!8umyL9wzN|dUX{#yc_0S~*x zW+_buEgDIR4< LLt+ApGyEd7v=0N6C@lP>2LYrT%p)~OdH5_i+WP0-nHbC z_}Im<97Dwdcdjd6S$8a7-Gs|0S`0t a-A2pF# z-c jzvMrb> zOl0)yIx^{$hcu^!$X`B`K|dDkd)X+H=wSmo;uhEdTDj-rKbJ$zb3rAA^fhEGH@`Oa z4(WD{=O*;oW$l|Ab9}7y+Xs|x`}!QWCO-qP6@Ya9_2>g{txLx!a*)v`U|wiZa>EgH zG})TcNzCeZ7BKBLX}mWBkYqjC*vbUKkGxDo&|+psd8fpH#U0k}Zl6-}-?OtXGYj99 z=#WI->H4-2gaD?FkHHj7Wfb}-dC2Y~5;WvEuMMO_^n$lNa|fMwTvldU(K#C}=aF9C zz9PE~DbH2%HMdo}DU;?37nUcF8bX{`?&&~ a*uoceFd3vO2v!J-uxU8f2@nump<# z*wtD-!Q5{PRn4tqbAr|M^d24F`@5~i_EVh=TF6+ZGn_5KP*P&2!X{GY* #k0I=Eh&)_g0A3AK1&&4;Z$J$4 _U8HnAf3VNcdhuU^I1 z1yPs&wWfm!;SJXcIKYH=d0jYYt^fE=FE*^QqI9hDw2=3Apx!7SU9nDCv}eVvRy^As zEfCEir+;b@H(>S(E+YV@qHx_ArnR>aaof5gi}40dHKo}bfGbw}7@wtzsPADKj%hx5 z3W% 6(v{mo+hmD*BLH9TZQf}5J2RXbdZLlo#)QA-C(bRK5i5@AA7oeZTL^-~P6`xk^6 zicv`%Y^P*@w_mYshvAy%s3^!DYFzCDF<}=UU7gOW!R%s1z~kbLwLx=|Qd0I}7zkqE zzda!Z;V?~a*Z7m7(xXEj{7H%ZzS2;5ru2dAtww&f)o)`I&`>O-usYh#6^BFn`RTsJ zZXx59*sP*5T$m{}OIx*bVn$flbT#b}P+ns`Wuo4!1&ZwXKb*Z~RFq%%_p5}Ult}ka zP*CY^P$?w^L^@>X?oN>|0i{Dhx `1`rq;qz90OnX`xgbIy6zIxo&yhZihl z4a@tU+4tU8e7~Qo%Kpb*Z1uCA oRr=o r$02ux(YqYyy{mwSh;BdF3;E zs!OQ$E$R0{8QVdXXk_Kw>F0QtLv~bjvk*~(uQ+(k?~gajDa<9587a3&wkoXNFo}E> zs~zx9VW&tRg>2A2|0f(cwa3|S1l@~oTmF62_A^Z1(fafq2=V1^vN!xU6;-rnTuyGy zr+htnG7l6cw%s!a|32Bu**)J%m%fUsY}x)ouJjmnn(OL4OvP0&JvOsv4>WazH$oBJ ze?WP8J7ARFrsB3@Xli9kOO9`);lR`QRuXQ>>ke?jD37 )y_ zo7j)?SLIKTr>Fx~f)rA>oh(uC$WPtimL%?fo5OhNu}6L33?nS@fjUy0632v(l$sts z`?ykX@aKE-N85VpRAxlv<9n*4@i84 jZJK3|&3wQ@d+jj{dzR>iD; zGxcZMuY%&SxO2({eF>bpL!_|WM+K0k+_IFf*rsyHOY%^6SGdA1>VYqw!bdU4l2+vS z%5WV{R&1_fbh>U*NS@0nGAc!Mx1wI)em5>WQH|}CqaiQbn=RCG#HI6M_pPM!Tf^=r z@zsa$8u>Z+>TxD-$L30T6jHBdpB914N53C5^sMqb4xV86WHBDM$*1KtGPd@Am9_pC zC8Fl Re)-u~7s)uz;Pb%C53|#A2NsWZ)yUh*$!xNuTQPz&)ju}i4 zirsWU Oi9I^F;-^vVI&zr=gX_6WhyvcQ(oF6_u`y-GC1+s&eplX%o z-xLl)dde>t`}e|F+9{D&hGOf)ub8cW<}=l(8 tkNBgl>AnBDFkV)LcppW_*`8vw{FIy+MQ;LbDF#vz z3cxUp4nTxtYAp53F4vUgk-7=3mvHLKs*|rQBoJgR-er7r6oyQZJ90I(GveAk>#@$# z=8yeL*%GWV9#EW8rdrbDt(h2J1>fB$SneJ7kGG3QlfK^iQ1? |NQL`{2Rz zxLGDIfQT1w1lPyHbsm9r)!#}lAI?UlH0T^S{Iv< p##7GNLNi20 zYfe;28-7_|=}xhRiQHD}jK)ps@;a(lK4M}}Jg~^zcph!O@=7?(vSpgq!JsWRz0#Re z)-0`)-z1a6HI|xv^gD6__~?T1E8oCUusKkhE52k)<}n%XVA>$@`^zc|%78Z&9F8Yz z_b9#ikt6l?rf$; gVZ3>iHhHhDety gu51e99|_ z>F{5qeart)Is51}O5}1jp;4LWWTJ)IOTSr|SK->TS-9#~m-7(tkKg99JvE{fY4Na} zwrleU^sZgv53Gvu3N!5gnPg6r<9!{ib2?h;Ol)+PJ?ey9B5^pax5#qc`|Yv+o*UCP zA0H*G{dw9nDJ@Y$0KC$tq9#@I8K7<}ha%e0ZJ)G941Un4oWi)&eRP3M-PxIbyJ3u9 z4Ya(|%BS|ec&5f_qeL!FzYR;!t*=iM3b;=2EAVC~&1R(3+y80joVt#kEY+^HKC-Al z&2v+khu8y?cD7Lm{B)l3*C^rt50AFqHSlNiv}*?4tFkB %)9Iy7GfPHmM-0Ot&Xp#3c+#Bs&IJ-3}3wG9&j|K0Pv#3>jK>aO!sA+&WYvjTh^< zbLZ=fDvz8~HWQh0v9)pvsOPFM4wzRzf`7UIB0m}Jhs<}ojJp$9Xgob%_DZpfE`IW@ z-V59A t`7!;Q&%l(0N+ zs$4k~bDE`T4^R-_k-95UVd(ppvppxJjnO6>T0W8w6%YH?6ry%@rMM+j;Q5+r|6+*( zw)m0dVYP|mzGU6;Rp+E+9r+f`_w4mqa*V2~nH+*$kR7Nj#?>tw+VcY$DVSvG;_KSP zyrui`XpX` o&F d_43$t}ryi$RiC z@>zl8QY(Z>7qHv 4K#?2F%PksNg&@ifCJL@k+l)jwTaCHSC z@qr*>NTOL0w6#((F%y967%V+n)NKE@ix_+hTRM~Z%hfX{Xt0*AP-%a&hkozI1nqqo znvFD4eQ^S8++vE(U4aq?#qTx0nA5afA9UNzII*#1U%5STyXm?9uPXSE=-NGA 5n>^A?Gte~{Ull#GgN_I%TXxHw{m zR@9&RofqI35&NI32GI-F*)8N1xN2pv^;ejGIxC3a4rau|RU@<@+1;vzMazFxY~O z!sj6`X768SjIfvbVV7G(m;30=HjbtZp+E?*@K7Vv&DRFL*jcg*FVv8tIU2DnHon#x zVx*~#!w*R5WGp0j4XMQvBf8dmv(7$`pAS}Y19ZTy#AX`z*HY@;2jhYT{MFQl-^`P2 z p|EvVjA5^i=n)C@I=&xgFP=qo+s{c6%4Ad|74G!O^4A~64p&f zKW+hZD{ERiV195-xVe=KOfWEt)%(MnJwUha_0^;m^xf=I{A@|AfRrDvVZKDzpy$XB zd{+A!DyoQOT6=-1)+ZIIFiu$Wc>uaOyIUN+VEyPdRl%qGF`F(UOx Kt$4fQ_8kb0USnEmUx8T UE3vU$8|CyI-Mqw8jQ=fkiNyJC(a16w{!hh(=jNs6*;g(8 zs#sF^b9wW=cdco=DNaAGR3_qRsB!KE$2|rpw@9&z|CBI`@5==yrp;3M>?T|gfd`My z47{Z`K~#{(`^yeXb6~+{7h^#BFVGgY-}@0Op&Ide2|n$o=VcOg1}ZG3qsS_a2kL4p zB?)ZH&*FGeCF776Cklw(FKy?zJ?%|&6-(XwS6! elzLQVh#(WISYF&0^|BY?2IROUr7!-{U(t0Vsp|Ac{E5j z8<|V!`bz_}EhdQa{jbT|?5W5T+kF_QlP)sekKdN`KVu7^3MNO&2C6^m#ECDpfb^<< z=1KJMDGXK!1Etcw_7j`=S-)QRuki%H?TUie *CjOala!mii1 zpHe=E;hpn1iXD&Hyf+nAD(Lu8Tf3TfcTTgkAkI<>83bb9{!y%S2~b~j{d98+yciIi z)*HWVSek5B+*u_p$h*2x?49+WdX3L*3hu|9)`Uv@n3*zZmM7b>&rM==)`vLjwaSY! zJ0{8qhO|XiB%vb~OJ^h~hBSc;uTzts%dyqp`Yl{^Yb3!(Y^|xkw$L_PZ;s<9`|#n= zmUA6h&hZ1(wtpqw#$V}eB7Vkk!7RN_3lq&EkZK==-&_+Z-~wUi+}@LPOt;m2yLb9a z0hxWoozKrZk*84`rVaKhy_@|Ckk+QD&h9(OgGE;(9;$)Y{UP5-%N0Ni_-|^J%DNa; z!IQ3%Cwn07mi>Nx^Oua)t2)xzuwp4j^eP9~KS7L)DQ sZQqAU4UP6mP)l_p2cMy1mq!d_uH?}phc+)?%ty`>C8r0 zNeD@NdkIbE97;y-IJyiR#*)y&2)YOKt*J%dc{%-D9`l*-P&+D9J%XEHgZ6Gh{$3>V z*&nBm+6oWU9SOxUia_V2?BU%B9t%Bn1Aap#Q=rucny{PrxDZ%v=ls81s6W^C^>k}M zU>Q_zalV=5ZJcf-Unbnj*0a7FL%g4;WLRD(fs&*;+Mt^{TC8+C8XuKN(PH=YkQ||O z&Zx&_hG`7_W~N@~+9wx_ItK^2-CfNse1QRmUt=$dmfRDF;3d`~8tgfnKu)@Vx*ajC zF^fml*Mtx-An36t($)l}_m^fxmY9{8@cu`(kF9L-b#gLnLz%hro;D#I rZM-7M!TD{+^D z$J^&evD><}7DS#EZCU&1{RMAS605|ql|cOdJ$Wy%GY-gim;(X>)k^$qR6m`mF&P7y z!{-b+o7iqQmAd5R%ues7*Bb`o&~gD+5Wt8J6COV=M--jY?)1fFjUN%x#)sRGL7I9V z#adOSRcLLaN;iG)gJuCU0j30ivCq91Md}=eT~xQ%fQvjSH=L32eEPmKYrFW}HobG7 z)XmNXioKZ6g%aw5D-&Suvjg9Uv|q0Ok%P#oMKZM=RQsVFJr5QkdmY#$8B9bc#wZub zg|^iOdf&MviGSZttxr8XE{3@S2I>M^qLQ*FMr*Y2>Te1W_L!on*=>*xt?yBn_hMf* zcq+J#ek;8Y`S-Q1s%bEg?u`DZQXhVlnq;Jw*yVTDxp9z`6OhUhCjy s#!>?&Ly9Knw;PtDscY|W3; zo(4>hvM|ucWyUHHBR^Zm9%hYd>gWMhyL$HcNtf)v02MrMx8(htbu5wqBq@EsS;y5H zH+r=l`*iJl65~sh_-E#GCjnz}ZDo{0&>NzkWIgsyQD WluzUakL zuk)`Lr~e(nu)p087R1yvntXUaaMa4esm9G;Gs~>f1W_v7*~W)A-5cZLH54}Rf6T<> zMeLmUX6u4{|DtkVJ>99Ug+H=Lfs1;7vJB2~5nU44uKNOW5D-LR-cIkLgVrs8GP|`m zwZ5zTTR(_^E8x)#ygbR~AmEa8R&3_PKFMZm!2c;Zf6dZvlI@`Oaxx>n)+qTwQ3Or- zIa^F2@0-x3!t;t09)|f(-szds>tKbl4okE0J*AeBI`(xL&Y$f#Uv8lv@mLxSj^cSu z5+Z$uCA{E9S9-b-Zd% ?$vlE3+zG(=|>8I<`_lU?&7tNXPO>rgUL|k zn~vF96vdq1(XiWFVW+ zMsgWzC44Y#MidIG=pQ Yz2(fvw%9%36Z=DOUMk!z(SU-!NyTOgq2 zdg gRczC2~XiXjOrshd@>Qy<)G?^{BYl@1xc(AkG5vErr89VTo$NjL+8kwI~0fw!>3g zwtpSZ9Z7|gQAPc)2+3z9{zg$(Rb%iC%V3z 2FGZG3uF9iedJ*=UoX%tPpVE|>;L0;cniMt46Xy3A3e17_Bnsj z@lsMW%ilTML2@Ws+&ruznnC}>^k(CV1AL}n{$Nk6(m%}^C!)5yiWRL}#&*;+>-OPw zs6k{z-d@KzlP^e*%$bH6`?K zJGKWrH=q5hEzR3-Hty+0oT5t>m}0EnlLGd=;IilBorMbrL(X)&)`X%*eoM>QewiK~ zr~mQWE}f5hJ(4y2^a%CPvYwymkG+J<2zB)4q2RF`;cLj(nTow^=DkW9!ja3kD(VoT zi3iLOp6sRf3Bhq)KH))MOrt{q!^PCBn9i6y<)O`kZd|XGmV2aTBfvf$@m~}nTsUoR zJ#XP<9VppXC*{aIjqAHN^O*(q;JOBsO2;tWAwE3NfRd4-xte~L3FI|_uZ-{IWv~CR z6g`g3aArxeEpQZ_b%bFzKwRvS4eJw^wjIIA{CLo`W_gw4ua&j3|2R)thD6q^2FrX? z$Pm9==1aT+whv8g#!cjqWVe^=G 7&xWc*;<11DB=>W{IP2
)~jZy#1GXww05 ZpS^K30$0lm`Vo6^lj&AvjVHZs)%-oSE}X;oy~Zja z*CLyl@6`#lIcD96N#E&S`T(3F0+FXK!?|tgWtLalGd&yOHo7J)CeK0i_&>BB0Cs p>>xL)@!Dj2NSdJgw&EAjmBLD1>G14~?UX&! zlJ3-elwh(d!9vT1jSr+{;Vq^n%Z5p>0NDNEZKGEKlRgXJQ0+0Oc2Dh--)XQQl97=y zkk(s!wZH?O6cD&zGy|^)1X)9*zg+plPwl`%Jjbil6dCSdX=N25*q!Q|{P`M_%|)kR z&MnHP+9Jt*d}q9^7k0fCq?Je#(QTus;$G eJZ*M;Z^^u `33)G|a$Pr_AbUq*IN$-DflZ5=WF&{U4a&7?T78($v7A~LC GuH&B;p}@oCNY-y0$Ijy;&ibd*ij>ke(q{VFXHjTugnfkMOCs4iOC7 zOA6s(^5M U<*){=^jUcXG2%TvjPmH>YQmo+F>tcyEzIh_LrZX+@M)T1cO zVV|Hk!xb23S!EU*Q54AsNYmlVMO(pok_5sF&Y(4m(&KcW& c$s64HX)`c z c#1j&F&syeXl4!!)MlFM( `@F^SE!!mCzX2rN5p!FJ^NkW4SGlDma&1~wgvGf*P$_q-e8S~qavmSEWEygO9bkJ z-wc*!+Si7v*J;0|TTJ|pdGxvpUgI9VLHSp7hiO*6@eDo;XnC!3e2Fdx &QU%gME>6{32pkG pJVqSk3 zZ`n@1nnYNNRCU6Ac;iR62_QB3%k$ zG3NhANU8GF`8b9|+E7 zC$(Ld`_(Mb*i(BN+_K(&ZzVIXP$#B1$I}md3*$F<4FyM2cjLO&c?}VIAM1Jm;jvx^ z0`}$CNz-M79|UI~-N=?!e l? z=_(w52_p;2!Bedr5cQn>AvDK|`7BO=9shm1T>CfQ!}Atg=yJe535NFeCPVD4reh2R z`QeJ6W@YjF0sl`vw7 f01#f#F&VtGyPXmv+)RSFn%1EMu=7xjg zZ0H#AS7NTiiKt)dKhbLDypDfb#@h)#z3W$~Rg-d9`Ah3aW*<4t^BW|=HDT^_;UUAD za0Y!g!Me69l$_tewd1!p94-gv3#*z`*B?Y0FNLKTs`E=fpB0*m%h0AWGwLc7Q fu}IJS9! zbhN;YCoB8W5u52DTJQ`T1bZYrJrchfU!!@XNuWMIA)a*s(fpXAN*^OY{xc)pzgjBd z2}Lgtl%Vw9;?y|NYuSgRp?_XwYm-Nnl*9roP@)D3TKSHBO}h*Gd}f##M%|y+#LAbi zI#<=U?%}rddm?ZoLA6i*C_b{L`i}S;Tgqa&T0I8zX06 zo##=&yUrcEf-;Zsj^|~~uIiDY+n}e>w+*Wglv^3aE_C_CyDrs}suaWPNdpOs<(;C2 z7ztOj&Q|u@jRC75W>$#mqEh?jE+U$PIOiI9d45499nKrpo8$7#(Q6d1(7!@!E#&N< zTuP1oETvC!d1i+pK>pEJ-8&;hdYG^v?<|J*q{?KudIGa1`J$dl(3PZ0XHTj&HWQB2 z9oQWaLNAi@U2Vr(JUu&jSNEY0L@2 nW2Xd&-z;oYuu8!? bZL6&$@r=sm4z5cM$gEPbS`$#FskZUY2 zS*1bzOj70RLCiFrJ}}aQ8%s(>YS$%RnJsGjiphAm5>+3UjgXR(^uiq#*$kd5orA_U zOcPWSbg3UdS4bz}$S9h?d;O4JYqDB^$!cAEgd7vOxpM?(li$n6udFaneauC@WUhQ$ zG0FB{d3)0b&%QLnCi)~Zl SssN8fZ$OL$;K`3yJhPAu#H~W;%ScRYw~>zwgwOm}3fg zqRKAB{*$K2o7LlssS ccXA$p8wVw50%zn|CUYRoSpt~iEiPXGG8MyJKG4gk+@Lh zupIyjemcy`dcCvOAQGXAS^h&FwagdBDQ>f)J)*f-zX!c+e3bBBLGT!{mDL>=XR7@0 z7oj7=fr3GP8f5#`ynbm<@)JHp*)x91; KC##eLL7F zk99pCk=EMo6&!98=yAigq=QK9WnS)PXLpHu5+U6n$}a;RqgvX#ds45nTxV%>>e-Bw zk}<-%XheL4uu>2ccCXqt&hT>OmKWVlQoK~e_NW(i8*1?WsP~R@nXVC3Jei$yUBwLG z4+lZWIAg_ly=L2LAKZ3zEkDwT^qi}OWd=&G1y5I(E&renAa;H#ZCnK3Y1H;yWlFMP zP|0g6qP*HDOQs#J m24z%{Z4=U3C{%~+^W=Kv{7|=MHi}Z zJG?XDJ%E+}=uidJRe|_88_GcQj4qu?tUA5*sxTA&opf2}P+EjXmm3gM5AqzRR`diy zhuc}zv}DbMj%gr_qI=gp{_S86grMlUb~{g1kErL1q|r<1MGlB^j;n*~$`#w~7XGSz z&rchMZFfbyOYnqiYPkh_U^#xs8Joiw4m>TD{V_Lh(w~L>kHJiGU?mU>+5%JC1r15C zrln=rpST3tYs SbliGmAWCx2s-&k#XFBJisk<35H-i^oR#%^7(GD}Gm z?xBORWn*p3-GL0T1q;(ch+Rjn{XKiBA;fr6Sboud7hX)~vDlYE#@!u9bq>0{%iXXP>{-3JtylAYmn&zX!{{ zx*o>E4#Z_#$!u1q0SEmwp$%-7F&%AVDEiCq$vT-5aXUue^ zqxNiWbls!!Dh${uTezKB9d3K4jkOkIn&f!LRAz1gpcbY=Ek|o?hwD^QpI<)Md}oPC z+bJ &_pLxMlYHO(>?HhVvqQ0jHx3FK|fjdmw!PjozD8|4}efFt5-#L)WV+X zQF88a`Bav4IJB!i;hFK-7zH&Ux5TDXllaA9hqdz)L@9N^p5dJbqg6E(kI&uQjsIz% z^8LN1OuBFI8^F@8#?2L1`)yEGWw7<=i6~x-bU6750PrpL;gzaX=>62}f +b7-YJ1K?!6D@3^T6BYc!*ammI4A2yq@4&VT%; z;8)6x-1I?2CR)Cv575~WsTI^whA5cjY-l4|vRzBmcX9CQ?{7aupK(L{VRw8Rxxuh> z-JHh@e-X!aw(_aa;(h#{$i_Bf62Va;F`p`uR^y~66*%Z5wW17W&|JXvn7G7Ou<7ow zfUcWnTHdP@Odk63q0T*{%yYow0Zn~uoR@8ApN6s>kJtcp$nn1;I4$7L6nT%#km}dR z!dqx&tREzZemI|*6d-{6h&|X}bs(r^9Iu;AchmsM_HdxV!;mM33SQoKI>87+v5mR3 zU4sSc+T>=I7C$ar)zmM;0zWQo`Le %M>Nlhx2-O~5UFS$tEzex+~VkM@G|K}m7agpjWdy+@3jx+lHT z3(t)gG(2{w*Qd~)MBKKgVIblDvzU%IzkQdLZ=>$K{8`Q&BLtjb2pc4CX4Rk}YyzZf zO#hDS+o~ClGA-$QsoF01bNgI+HramMG}v}Y<+2dG(GW*>YpoS~S~Rz@-xx7h#ywDL z!$>JJ@vp#ql!uz9!Jd*y{5k#-{{C4zZ8qo|$>n#>nCcy&_v8H9rrcCnGe?{cF#qd| zjB5Q(@#Ix(D61p_&u~Oq4y4oMcNGjCXN^ZMN8u~)?cx0Hsp3}~Hv5F}a?6Zh32GBe zG1DQUYnL|eU-%?nctkGq-P!Z6lsQU&nxNninhT9O!~04>+d1|!K@WqQw{yaHaB*#h zm+PYd?<$> h>ScGh@AI>R|L+lgw1-9q- |WO6B466(+)!2Qv_i@ZSmepIj2N8l0!8|xAmR1ypf4BJ rPQV z0?HYbpeH}6Sz1W*n% MiI$MEr{`(#{QP|DYiNf5sH*JuFS*mFj+YbA>iuxY^^b}-4VXT#Zk5G9IFeOe z3s8!37fak!?Kn#EuRfG;$31)YlJdL~X%Py1YPdWVpqzY!W^R8JyRle0nGOoeLxFPu zx#K~#zvjEg>geHrpq<8#f!0AhPghzS!q>FZuZxu`s EQ|haa!%2@bcX(!?U8Jjn#Z-mA)bCD3HLgDD2)uHZJ3`*VQ# z=W=M;(ZA3g33YwGji}_r42ISHYGG^RwRR-En#I%_6!)^L!N`T4MAXZ?GzCz$66AVh zJx1vu{7a+vg_qRQNHL4}+XWB5lkM4_-(O91XN~zFH3P#^t#lg(HVVpA&;sqMUg6)q zuCtz{Y>C`A%f15?knehp*_F8?|M;J6#Cm>$YohGi5HmU&nBp}J*{te*P$G>#-7z%j zJl(FdZFtp&0Mmii1rr7@g1ATEJ{$g<6n?0zjT#vLGHx#;pZ*}GEXVwSVtI*U(1V!O zwKkw`=6y_~L;bsqQIIG0z2x83@AQ@2d v`YAB+lDM9>3vO z1Fd8*?Q4!EVqz=o330EnuE)$^;|7|0|NN|*-Y;G&^C+j%th+{awDyT0pOi%Bz~7)E zN+5Nq4%A88#j(Xs`YBEqGJ;iCbf>4mh$?}%qklf^k%-*frzLRgIj>{_5+j!mwgNO+ zhb$AIjeANc=^U gPezkl)c@!p%>ml$Ah>XRuTeVjr3c2sPZ &Ph- zcC4=uLi(m|h)Kbm$nT(Ye<{)NnhLIGbRDdA6cjFv)yGoulB|u>X{x@YfoAWS?1YB! zT6#B+fl9FtoWC7D*5t{Zg{$ da zfweq}xnTNg@1k);rSChvC)e==zwH1K2L87Z%yvt%{m$Z1NJz*&psVfQ;kv!25@5gX zvuF=~MI{*Liv>zE0naJUo+1YT*lHGoCTv%ty}Y>Nbh5Z;Qm(F#<;UnnUxn*63OH7l zJq*6ahfEbD|2vwH{FB;FVN{>NR+m=0@96 SFC=_#u`7Pb)VoA%vu-= z_ ?H-iXg-xYrfG zF%M(|kcLACb)qAOCZv|HSQPD0>Jbt5#zuNoB)X5}Y=%y|K3AHM5EG;9f6CA}S*<7@ zE!+YuJe8FLoF~|5T6+4ci?a~$3+WdD6uv`8LGi@I#E9vlHabv1s|TDjuEovvYXA;w zzzW>j- NkC<+|=k*MkUo53Sng)iiC<40~2fgssiciJG;FmJp z+OX!mKkfO_7q=~8XZidtdm|MNjQ#_)7OMuJtthfOyz4+aSKVmV-1t(mTI`anpQ_%v z1rI~F^lzV8RoT7ov $V5Q)Y)vtJbG#nz6!yz~!d@43DPh(vIKQitq_{|6=U zo|=;EdZnE2v7R0T)a9%Ex?SK4-fVR?UoIt)FaAfPU@BDex(Gc#E8aT%{nQpu+C8U5 z-`Y9#T?hsp`G2DntDFsX|96gJra*E%xV|Tg!>jvijN?RhjHjn6vC_V@b)00XwZ-a3 z4C3$hXU8{w6au%qHjW#~x(J}5ZARrr2&go}D~%Us_pUj3Ae9H%?%VJ>cNW*lHnd&$ zw>Kgd@~RfiS0UZXY^gp_cq6$@Ho@Qg$8Y`)0R7SDe=jF+)3|#|qQOAUuNKTOncgRB zNiF`J1Ih{knMZ!J!jG$hH!Q_F$UA$}1YnHECd29Y8&{ewKw%adU#r3b<4|{s@-o}` ze(cla16{nNI+9Mg%JiKFjh%fI$y`2Tl7jQ3WD+7eHj~YfUJHb1fK5`(8+>;zx#A^x z%tVO-D%h+U44Heil@!Rg{Y*zAyaAr3vn;|~ot;`C!W{K-^dWOp0oBrElviaRt>dDy z+iAweKmP{YfBZ5^o0ege)+bGdV4PeRWBy}h-Wpa0Dr-~a!3q(zixKG)$M2ItJCeM+ z+`ktWzhVbWEi9mQ7I)iidnQX(_k&T^U`uhw>>KfuV5O3<1-AVo080|>Irp^Te@8V6 zI_-2_DEqJ=>jC8$@#ly2_%s?;7CWo+*TMSq&Uy)pc8trKVeqZ(;sJSV6^ods?pUo? z{SWfkvSOrZ`3g)Lma@L9kP6M?z+F}8-Y-A8U##_Udbv%dEVa_y&$s`pPP?>EUeME% z(9=zuvm9TXx`ZCL!$_Ms^Zn?foo @0(uqq-|oLO zn-(xQQcdFOPF~uw`ufmdr24v0eU!ezZrWYZkGDo-CFf<9STE9QE4F$of}wMAqB(nz zdGC+VR_yTBGo7$fn#qgRm91{S7WGk4-+R0?aq6Sab*55x=d?TJ%!v1W?oiM3Zk4-J zu!u!fpPo;8_Ix_exXzCI33M6-bo*cQdL@%cGYikdvKo}OE*%K|T88O Eb8JVnJ4aG``m`*oPa~{PwikN!u%F1En88 z%>f?O6?`;xK>(O4Z3keQ`zu@8vEnEHJ1%lB$!6KVT|wUo;cW|Oq6)I=Zo26Oz4m{8 zY(~v9J=FXqgCbd@)jJ}{^WGg0Px?D&+l0QeBh12PN2w+Eq3w2gg78!}p|#x#wQ+fC zhE-X+mJL tEuoXv7x|uOhM!Xx>Yq1914=aw1vbJ@4Aa_{4Cu`FvkEm`~8dm06 zw=UZx=vM63{jXt0Ec0lCdnz`MQm0Y~Zg#<32-+@M<(3G&Zw@pn*Tbh)pL;)@$CbkE zSxN?t2^h4CW(#~HSAf{{$wo7!IWXd-s}g-}ZoX{i)UQ{DT@rx>o`18z!CXq$C3jn9 zGP||jh2`D!xrLT+_BFKe(NKZCB9X_+^)CRjUDb|d!pfAB8oRs=ehidDT_*9<9%~E5 zwUC_q)+~sCeP +w* 9uscyR^A2cNp zR+#nsER&$%@am*%YqC_KpRGViru^M3u}S&7lzu}$2jz;x!Jm@KT{9BVoL+uS5QuO* z14)E_b8|DBzwsoPulW1MdgHJ?6gpKNxx;A4PD0ExJHJtym(8o$@?jUiDAfewuBniu zhxp}rK_u9xkJ=`jiT+O*FX`ccL0S8v8sy*YVa3jxvb* zRzqSN(`7k$fTe9S^WY2MOQcu4-?b_SKx?7zqX5x%or*}948-`jqg~^+L7qnQh#9L4 zL)51<;ru(kV3`phi=Ll1v}0on0_J6oLK0IL-Tr0(L9>N%e5E6E{MJ2&z{gH*MO@4T zN{v5V4#jOiEH6tbKsReK-qOh`>-jXX^jP4os_c5HWn%Ab5UxjXSh3cmN^Vy58mFDi zok;Q~5(v)mtAe~?NSi;<*Xv2apvQW(W^1t7XWD}|&8l4^H8H}ly*z`%mYgGKFJq^I z^vS(ktE)$Q%MS2bkl@A14>zR5$!F?(4eLPLw>_7-#rc+iNQ-@)Y!Dle4|1VAJ(sOK zwj3j=U #P4y#0C2n4Y9d6c zjI??0^;qa0E 0{r(QIV581%)kBcg_G%JEbUTTO-M=b&B@Ubb%{?efrh;djfpros5EqCJ!CS6NLP znfZ?f3WM=%Lfg63(lJzqA%52SEHd=d>~j#OWIO$q?hXsg*rcGs7?}8&>MWqT -x7%fnuCtJw9zZ+Me+f4qW1~HtTm#s z>aOk;RVwr=Bk+BF U^ zV|aV0l}FZ<1w>Ob$&EhZJP @43v!qqLdCw2B8p&&Yi-@)@vAgUzAb;@_n{qDP`^qf~7mtO?v+-Z+#8?NM~M)5!VN5A{T z{EZ%yAAbQ%ZjC4}<)4svC0`TFI&iz_v(Gn;tzO0AdW|iXBb$w<`K^_o)88o?R(x(6 zg j-#aLA-j^TXn~ZCyiJi}T z9&QN-q@Vi#!@EKyErivzu6&PP*$g~$llo`Ow5}WYNQn2n^W1K3VU^`5 HuY6Xe! z@2@E=;35gry9K(kd!UCF>vOrdv>wvJ>RVsX_Ip|L7G88nYZFPN{35?%@$vk{Oncz1 z(Y!0WQ}#s}*B%g%Gdoc&Dld-U#Aue@6_qo%4 Zbv>c{u}cXrxC ?)kLK_EhA!?7c zTSzvkHs%U%eb(4;MZWlSiA}UPT%pW;9i*}KxE&G{h?jlGuS4|o%ifvx?KUmNti8ZKf5#b6x2k4dkhF zo6HGzyT5?$_~Q4EY+<9~R3wE>`WzKvH1ZsJ-n4)mu#`;Z*sgj%SPk22LCcko(f_Nt zu&|m>%qe;Z-oY}hAKX^%2_g;j{Z?9;cn0gl??SF%zG%Da@LJrBIj;N(_xa1pJ=9U6 zH$?5NVyxH(YqIlvc&NZM_}jap+$7h6< 8kgi7cN_xN9j^D z0#q1&*?&Vy*O4Y@Ajq9VnBn&r6iKI^r7}BwKD>~K#maxnBRBU9ee&Z2d*FpXxzpim zwrap2@-Xlew9NKI1zc+_j~NSUgt&T5>cm2o#p3;+E?0lU|ClGZ%$Fwd9a>ZLHOlro zqi@pBa2sCoWPz=6qAAFhwd{lQ2g7+^`8Z+)WQsSHmb9j;_{tQ=DfG3!A%7eB>v9T_ z!*Ok$LlJ{?x~k_F H7f2girFO?8)iY2 R^w9L?bQ1(rC-Fb|- z`t%1n2WUb9QO28X)x|JFegRFHcOsQNik{M5XNf;qb|aWW6x@mQ#vL{gb+WaWi!>Gt zJGSPCE=Nn2f&zX$@BLTfllp6|0-glTta=D%Z_ahN;W*-t73aD~VHwFYm|hx@)YQ~& z=HXt*80mPH>KpZUYI$FO+OiJ9X4%WSsmYXII^zB_N*KFB%;>8=yn1Li86ZN2^CC6= zo-tZtgV%;sp$I#2RcY`M589_(-NZQOc&5jXsg&Bf3ipK$`y7SHMwO*AjcYuL5h~5> z_ffdeF2#>ib7|7;h4*TcqJoxWg`$=_l4_)IVb3mLKOufOI_I2QRrFdp;51j^MBq47 z|9P?bwcvP*^jlZ4vgKz}+m*Ug#+>YB4KJbz27AcVR^-tCcpa{eDKZ0kn{&tSWc^Hh z`=l_@VCAh@gFZmy)U%q1uH=+p^`CxRW}&bH4`&{xDK#vp+jFR5C2p@Y1p#EjfAk zT~hzelGd4RTD8bTE``zj0_-j^{lR^(hMKMFkb-Js4>ILje;5$M44&q1WU>ZGA-P?* zJk$N1gs*m1_pV*H7qa+I!Y2KEL~izjKQD5j*jDA +1Q_ G<&^CvcfpAO(0 zoN{z !KAD5fBswl&+xC<)e2L z5m0*X9jT%B-lQl92vS3@0YWH32)!3+K?qGs=tO$&?cL!2=FOXd8D}u}CO2pAwf5R; zpQ9|j5m2>unj|@$b`F{=yAAIPJnezHtkdI*(-jK!uK11Cmth!)s{zmq^11Jg1H2me zx!s=Jlr~D@O|_M=IKu|r5?=V DQWPMPz@ZkjE;?OQU z4UH4YmT#;-swD}g1%qWdl%E=XiC3|bsVQKsmI3yAV`XV5){KeLO4W!|P!<|3R9560 z-uv?QLn?VnxicXqn2-}@;HYTH=zx2yY-2TQrb|aY{(+-FZQ^B_21|qE9kq^d)Q-6J zcxD^ha74dn^%F`4GYZ&F1j%zJt%m?+W- `8__m45GfppsM-ZmK?1>G zlhat?u-J^qeGJ*_?;%@h3?BZ6Nw_vnAx9fN?`vVon-QtU-f%higyVh&rGuG3nzTIf z @_ypl|YvG{@eRttDzwlHRh4|LI^i8H^Z(vfDmtkRhoPd itN ziXX9rzHy-ctND&k;$aBmU6jmcv11OmEtNGw{A$OOn^3U9$Qxe{D 4Mm=`zb+Th|Hq^82~(Q)iUho-zU#Yl@ndbvIFK`SDV%8Cu79dPcV5G_ z`E804n7kG9a)6MVddy;36QA5L!Mp4&N98CO&Ym?DK@{nB1^*=F$pNf588Y3d$(MuJlgh#^5;uAoX^ z3!UQiLMZiC*Rbu z^8AQ-&0q9wl5hXLD(>0q5 dxo-*A_tci3nge;8MVZ3z4FJm$qHU8EA|_Y zFow~EC&-J7ji3z>yDLwwaDF_lr~Ffs5_!Gb&Ko9kJRbhz%d}8jp$F39(UAPbTA8$H zN5LWH?&ATtdx!Y4x;pGWTL}Zf2o0%SOtB3!mn$ooA>MDKH%Rg@nHSNjE5_gIS15I5 zT=8ezuCRa%Jy)fh$HK}Biai7k+Elb!(JF^o>(;g>xzJOeeqz4YMjA34`G&(c I`%{yZM>gA7 rLAG(q^h1m_S2(; z!e2a;+!|O$20Dy)>m#Wv=Bv!4nX8&9(c`_B#6MKBT6kU}1!?@#lldb0%4R9`F&y>n zbuh(wrA9hMM(4jD|1>RFzA?41p@*b76HXzFx`~_X(p 95gVZOomJqN6Qlmhwg9r=*JIg6^`{@Ohv-mSVk3M#hyaQC*!vcPLqErGn1E4 z!pf0qJgkLNs&5}rD`(8&!#>8hc^X=N1q?-vWl0@YVEC_d5en #8%~W#rHVRsr6M>0^>Y}I758Wev8XS*uo3ZF?&nM_%E*YL72=jNw2=)9wK2#0R zfK|#CjTRN2x~l6b){DsktY-IBoNm+rB+X6vcK!D&8+Qe !DgDc{AJdbnW9~H{6Lu7DAv>68hV26=oTZ%=p7-QED!A6KOm^q_O`xGgF*<>2Q8; z{A$56%@-Z|&3>Bu?@JC!sf_)r=2NqHwqHN*%l8}5A9ef-cWrC=ZAy8fpT9|MoQye% zZXOXUB{jx#E>EINQk&XV{OVJ6n&65UcSB-NB|og_BftH0OZ;a)5eM3T5iBuR`~Fwo zeA*EQ@p0VmZms7)?qLh2X{3nu9`^omE SFU|ml6{4
cLxL&4wr&|ZeB4}`S`9mL@f%mxtgF5-EKlur_jPhKX?b)J~ zrSZG-rD`+}g _uQC-jl;Ssf0coGkz@5pP!ou~>-qtymt zO4iF032xxk?OW7i91nxU9=?#Pd)7(#R2$AA4)M4B>p4rFfRmBIx;cHNGqfWrQ6x<0 z72ZBMaxE*46gPAM^H{VIx qA{Xq$@9iXaz@91zg*x0B1Rk z75}tI*kf{hLPh xoaJ4Q5-OX4WbEct3gGDQin6 zyGqEHejQo-0csrA=(2N;iHMK`vOSF*#cI(OVxGcneE#1z7wZz#wI@$I?3&WVXQ1Jo z&B~#YU(A|U!=2a`rlGF`7`S-MdTuY8+Oahbz#6jBy=XdA{xFBxfCGhD%aolslz~`D zP 9dTUzTwR$ %_NSm*1?Cb65brQLFQ1f_$%~nGEOxV{2m-AY6J~yIooM0RocI0HaiO1rn)Rwf4 z=gZ0U6uX=3eVh$fXL^5XoF1c4sEfU%agOeSo45Hekp%X`JoWrH_1 H+ZV@RZp;rEabF1$BGgKp7*Y+2Th!rBe&IKz3EZ2DyC>GoD0tU&2k5B0p8HHol zp)2u(tPq+jWq9_;ck-+E`=K^g N2nRZ|(E;d-dO&r+3BKCersEjO*feL6P$ zwbzemlyGOvH21gk;_4tX%~BBW@w9M(NT`P*N+bBx#%rs_?wHqZ&}FQ&lZ-@{&hqes z*xgN~cOFvj?bD6s%?)-ikf5Rz_QLlf7Ltz_g<#5{&s}&R4c6D|@2uy(GaFp>iGI^$ zhJ7ZkcDm!chyBK$Ww|kF>mk(|9{4|KGC$>aU3!KsCZJI-?+NzpKx?>r*OQ$29q883 z0_QcVsO29{LhMMP02maQBX-wJAXfP>uAc9;b|(! YnM&INX z>+WE_>v6AZHFZvEdZIX_LL;E}cqa q$0PlKM(>&< zFqG1U-x{tv?Yvor- ^(wtNi3*14=`r< 3#Z=Ml@Eeu0Kgy(y6QB>91u9 z`yHg4N4C875>O`Zv_=lId!C#+rtXBMZnlel^LPEX27@b{ROc>Uru3s~5}sNl=r@uv zC*+C}^9$;p-gRiJP;nfHZZyOj`J8bQp5_}9KA#g`rTNARI;QeU(B~?doFa|YL5$hz zNzkXgcUr~G!a*H>r-k?4vbvId%8uV`;}%QzIyCG`V419hr)bt6(6EJa5Eh6C)o#v6 z qvFBFe$jf=^aQrPPH zXj?LG2oT*qehQ9q2GsCzKqSkL?mKSNBvhm?O~;i#hz^N3$HF&Byv+fPzv0?8J?46I z(~t8F=qtkW{Xa|#B?|mxUC_G=s$=K{_C4nbJfv&YjQ2O6J+5=d>2~I>Ds^QU1Pw2| zONjcZwET&E>@>BreMnY=6;js=JI&LPV9E-Yt;|u~9z||mP2xT9A#85EvN@V@CyMbt z>g%!7zRWjx+iA`&3oSolKj>^{uxY+Jec-tbGJ2;A-A}ipU&z0%BUYn7Dm5=H=%Wx4 zFmxk0oIgzSU(xAzC3I+?T6>CJak?cbJ%qX$uj ul zuw5wluVnFIqD4UMt3a`)pMVUa5))nEv!%C%svm6#$nmdjpcdu+julHr+a&Kc-ZDA0 zx?i@)#p%a(i}CQ+>?atm{huqg1zz*^Gq-71l)}c07u&khhFv*kKi{J<5gKsDk>( 1nXVC#QY>w0Zj1T= zm0^g;^m=RF;dA@C)mQc&tzr|-!`9)vTWgtqsROA3C|my1 cr@av4h7^8lO z;Wc|{?NYrQHw25d(kk)94x
|bcG%>uq1 qP_?$ zK;Dn9aEiWN{d%*6 iwudi3iFc?Cs7k00D0yNh^m%tlid6(d z&Zf#+;+J Z?iY* aF0GhWK z<9#>9?0vB1&yj7Z)BAJ##(E!sf$gruL|VeSQirMWf*se13KhXJC8D<#E;-8z9rh5F zBIMr4g1u^WS;3cbLBc_LX|2~|Py|MSEyvr-i1Zj1o=+ C-S7wjK_;C>DlcvgmK4kz_&EG6EW0l)TH#ZOtxmH3! zpO?$o%ZJhc9!ZdNel`%_QD#azo&{JQITrT+`>(5OsKq7Gf8~C21gLuT2n1Bpf<}vL zG@)*2iy`&G=$uk?FElNJT`DxXShh%G&f(rvZKrU+kJ|6W#j*%;UCT)gpF=g&og=ZI zpF)j@5}(Sg=quaZz1;NCfXU`-=JSKxE(RPqUIUqp v&->ul0B}AJQB?@oZ_tO_@|sOnGAf9oRr!_F1+2BWSv(Ss0v#+U z-kE3aG$}|Mju+s;XrjC0X73-^V^&VrRb?BsSVmQ36_B;*q7eTK)wOfkBJ2E-W!a+h ze(Gz%{1HCcB9{D-Yvz1K?XMUYp&MA1V3@bS(%@(Jh_Z UI5h&(1g-TbKwEz z{AX2UVXJCG`PhV@EPDG3e&aph0|Pu{G}`(1p)GBjo&SCb$#lH%Ni#;QsX=34bJ ~|hOk4vj?BJ1<^_M{Dn z@QgCLT%$mrHw|J(33k3&iju)Bn=R!5Em-Xbe7Hl}#MeYwI#wF|OHGbpz>zBUePe}U z`^Wb TI|>(EFX`eBn>REWFnXE^SK3;)6%-&0TV z^@`_vcN#{`Bug%3VISD4)|BS+6IjiDVde=CPk_g ;%M@Uvcey%MBOevoZX~K9 z4WL6KBVRXskHpLz^Us;L*w2Z#FvY7rM%F6fO15+~bZ_HG*I&~WKhS#Zn81i-xS!{m zT6FsOee!l|ODp^Lb|MYC`sKqFnQ|S&EXCJz{zz2Wt=W}Fsmi zu9lh4XGu_}9o&!8k%9Q^k2+thPJgWslv0+YAWU^9wD~IGL)D{eTOP|0E%qk&7a&Ea zWSs#$q{j7cWc^jn+xwI(og)%;u-bFT1VAxdAqbV~TD&N&`O#>if+;+n45uP4dE_T~ z#HGs?*wzQ5KQ3>T*4SCLid))f2L?7x> WON0aSNx2?9XvK;s)gI~Hf z-{7opUe!) F~x*Ul bU&c-fgJ!v{Ab4A zQE{8ec8>dUn^h=!fhwvKSWD;jDH8@E<4zeG>(u~X {u|Eg# zY|_XZBWsxX41zG7LcxS(tab+3X^7S>|E$z3QDJOfm>t*Wp4JhaYt#X*Owoh;y3R^D z%Wok8LOag+@fj}Qlhc;w{-RlHcjJEzDiFmD0axkj`g2dz{oxcHE$u?>pwElzqtd~0 zYZS9mz}YsjDdoRp68Vm0ApNz~?d9==#?Ac=_()`iCz{s&+I_{Luhqvp9LjXd=vG1C zERNy53v0v$aTZ>KI?Ew{?_dB#r6A(A@2!&{)isZxZJaWkCQMqr_pZoK?;F~au%h#} z&bE^66-(Lb?jE8n0EhEBk_lxy_B98M`UUVrAUrgwDNyVWNRnzKh0T3g1pjU{ab(~( zJoJ3B4EGwxDRV~p-Dwd@V#%+@-7c`rD8r!}CFW`W|JqZA)do7xZ#)d0fD|CM6R|uk zU2-bElU3aPE&A@c!@s1Y5$ gn`Qg`0lLoR7)G_6R)4?7N9uC1rTc-0 z8}y&d^zn+b+(bw~Ip&m~2)lf+85i_b(nFb3qn*P$-2WySlLH 2+Uyuw}>BoQ*9;N{a47Bb-zkx5Ra!S!!1s+(I?gP)PV(i7xu3ia}2Q- zexibP=g}pgK5%rg27^m^hx0Nf9Xamiel9Mqx@T2q!=($&Dj*uo3@f3a<{oUCb)Vt; z%WSbvU?v9F{q$53;2zqfV_)%j4cZTr4+nS)M^sCxt@vXO2gs@jqoGn=sp3tgv5!Co zcfkXhE`=hN(|Xi)UA(4}1(zS^W5*0bwK(__ChI<&lN+um-}bi+~3X2dyq-V~s+yVB1LBfd&Ms@XW5?VM$XT&LGk;SaZB;{F>FB6;FyusxCL^`p%! z*AaQ0UhZPvTf@ocb>we^5>E}=lq=hMDhFLwD%9jBe5J=mJ+yYxZNdm7DFI?p)rgdS zZw >G;D29tOL?}|dVB{X3!vA8R=<9>4W&G>yUc%Z z!De*wmQwR<;z?6nz1YDJ{%E*r`&J`|LvGr**^z=r13cDSzTT|yyp(nWY~_`p``SZZ z(IfdN?~Baa;V+kEU)#7*EI_bnK7%)8B=BXn$+@YuH3FN~4)qXmultl{{}2&v6hI0& z8+Zo9eEU@jtk<(HFQo@Y{U>{VIs>PoJVuvrRi;$>)tkrjlZPMK7}U+NS>jKw<>eRG z3T#&|h}r3>Npne9%bwP-uG4>u(%}F69HyR7z^2{DHlpI?sSnoEO=*5!(h1gt?2HS8 zrx#%P6|dNAjn>LOK 7Sf~v7iC> g)Lk8YFMv9RfGDu Fz=Si<8&Zi#D z5tY#*7h4ASjWpP>`b&fX$eLJYXq}~p^5fTyN5eOjaNoe=pn76&(zc Y1{^dH<3q^e$ zI`2m9Io8RUJ4BW+9{ZN8d-}4qdGHh4aYixcO<>}zyS4`;9PGhcGvwBghSgED5;xa! z-ZPzBlHgoWKFPHd&787tjB3+OgE#CXlu_Pj^T1la<@g?Dy!sW#sFggh&N)<|?$u3b zn`7-gfm-ad6skL~YzpFqUmN|>`KCFU%FyJ4v0o4jW4AONCy5hE5|)yVoeV6HgAytv z6{Jwb1!}VPwSh~uL64Mhj~%IozUr4JhE(ZTzlV^bdcDWFRU (~ydGUQYB##