From caeccd39d70d1f642ce61d6912599cdfa9028fb2 Mon Sep 17 00:00:00 2001 From: AutoViML Date: Wed, 20 Dec 2023 20:16:16 -0500 Subject: [PATCH] Updating README for version 0.5.0 --- README.md | 94 +- build/lib/featurewiz/__init__.py | 59 + build/lib/featurewiz/__version__.py | 10 + build/lib/featurewiz/classify_method.py | 320 ++ build/lib/featurewiz/databunch.py | 686 +++ build/lib/featurewiz/encoders.py | 125 + build/lib/featurewiz/featurewiz.py | 3878 +++++++++++++++++ build/lib/featurewiz/ml_models.py | 1818 ++++++++ build/lib/featurewiz/my_encoders.py | 2145 +++++++++ build/lib/featurewiz/settings.py | 37 + build/lib/featurewiz/stacking_models.py | 1397 ++++++ build/lib/featurewiz/sulov_method.py | 277 ++ dist/featurewiz-0.5.0-py3-none-any.whl | Bin 0 -> 129996 bytes dist/featurewiz-0.5.0.tar.gz | Bin 0 -> 130300 bytes featurewiz.egg-info/PKG-INFO | 311 ++ featurewiz.egg-info/SOURCES.txt | 18 + featurewiz.egg-info/dependency_links.txt | 1 + featurewiz.egg-info/requires.txt | 20 + featurewiz.egg-info/top_level.txt | 1 + images/feature_engg.png | Bin 0 -> 336051 bytes ...{feature_engg.jpg => feature_engg_old.jpg} | Bin old_README.md | 322 ++ 22 files changed, 11490 insertions(+), 29 deletions(-) create mode 100644 build/lib/featurewiz/__init__.py create mode 100644 build/lib/featurewiz/__version__.py create mode 100644 build/lib/featurewiz/classify_method.py create mode 100644 build/lib/featurewiz/databunch.py create mode 100644 build/lib/featurewiz/encoders.py create mode 100644 build/lib/featurewiz/featurewiz.py create mode 100644 build/lib/featurewiz/ml_models.py create mode 100644 build/lib/featurewiz/my_encoders.py create mode 100644 build/lib/featurewiz/settings.py create mode 100644 build/lib/featurewiz/stacking_models.py create mode 100644 build/lib/featurewiz/sulov_method.py create mode 100644 dist/featurewiz-0.5.0-py3-none-any.whl create mode 100644 dist/featurewiz-0.5.0.tar.gz create mode 100644 featurewiz.egg-info/PKG-INFO create mode 100644 featurewiz.egg-info/SOURCES.txt create mode 100644 featurewiz.egg-info/dependency_links.txt create mode 100644 featurewiz.egg-info/requires.txt create mode 100644 featurewiz.egg-info/top_level.txt create mode 100644 images/feature_engg.png rename images/{feature_engg.jpg => feature_engg_old.jpg} (100%) create mode 100644 old_README.md diff --git a/README.md b/README.md index 84805c5..d233aca 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ ## Latest -If you are looking for the latest and greatest updates about our library, check out our updates page. +`featurewiz` 5.0 version is out! It contains brand new Deep Learning Auto Encoders to enrich your data for the toughest imbalanced and multi-class datasets. If you are looking for the latest and greatest updates about our library, check out our updates page.
## Citation @@ -28,37 +30,61 @@ If you use featurewiz in your research project or paper, please use the followin

"Seshadri, Ram (2020). GitHub - AutoViML/featurewiz: Use advanced feature engineering strategies and select the best features from your data set fast with a single line of code. source code: https://github.com/AutoViML/featurewiz "

-Current citations for featurewiz in [Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=) +Current citations for featurewiz -## Introduction -`featurewiz` is a new python library for creating and selecting the best features in your data set fast! The differentiating features of featurewiz are: -

    +[Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=) + +## Highlights +`featurewiz` stands out as a versatile and powerful tool for feature selection and engineering, capable of significantly enhancing model performance through intelligent feature transformation and selection techniques. Its unique methods like SULOV and recursive XGBoost, combined with advanced feature engineering options, make it a valuable addition to any data scientist's toolkit: +### Best Feature Selection Algorithm
  1. It provides one of the best automatic feature selection algorithms (Minimum Redundancy Maximum Relevance (MRMR) algorithm) as described by wikipedia in this page: "The MRMR selection has been found to be more powerful than the maximum relevance feature selection" such as Boruta.
  2. -
  3. It selects the best number of uncorrelated features that have maximum mutual information about the target without having to specify the number of features
  4. -
  5. It is fast and easy to use, and comes with a number of helpful features, such as a built-in categorical-to-numeric encoder and a powerful feature engineering module
  6. + +### Advanced Feature Engineering Options +featurewiz extends beyond traditional feature selection by including powerful feature engineering capabilities such as: +
  7. Auto Encoders, including Denoising Auto Encoders (DAEs) Variational Auto Encoders (VAEs), and GANs (Generative Adversarial Networks) for additional feature extraction, especially on imbalanced datasets.
  8. +
  9. A variety of category encoders like HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder, OneHotEncoder, HelmertEncoder, OrdinalEncoder, and BaseNEncoder.
  10. +
  11. The ability to add interaction features (e.g., x1x2, x2x3, x1^2), group by features, and target encoding.
  12. + +### SULOV Method for Feature Selection +
  13. SULOV stands for "Searching for Uncorrelated List Of Variables". It selects features that are uncorrelated with each other but have high correlation with the target variable, based on the Minimum Redundancy Maximum Relevance (mRMR) principle. This method effectively reduces redundancy in features while retaining those with high relevance to the target.
  14. + +### Recursive XGBoost Method +
  15. After applying the SULOV method, featurewiz employs a recursive approach using XGBoost's feature importance. This process is repeated multiple times on subsets of data, combining and deduplicating selected features to identify the most impactful ones.
  16. + +### Comprehensive Encoding and Transformation +
  17. featurewiz allows for extensive customization in how features are encoded and transformed, making it highly adaptable to various types of data.
  18. +
  19. The ability to combine multiple encoding and transformation methods enhances its flexibility and effectiveness in feature engineering.
  20. + +### Used by PhD's and Researchers and actively maintained +
  21. featurewiz is used by researchers and PhD data scientists around the world: there are 64 citations for featurewiz since its release: + +[Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=)
  22. +
  23. It's efficient in handling large datasets, making it suitable for a wide range of applications from small to big data scenarios.
  24. It is well-documented, and it comes with a number of examples.
  25. It is actively maintained, and it is regularly updated with new features and bug fixes.
  26. -
-`featurewiz` can be used in one or two ways. They are explained below. -### 1. Feature Engineering -

The first step is not absolutely necessary but it can be used to create new features that may or may not be helpful (be careful with automated feature engineering tools!).

-One of the gaps in open-source AutoML tools and especially Auto_ViML has been the lack of feature engineering capabilities that high-powered competitions such as Kaggle required. The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables was difficult and sifting through those hundreds of new features to find the best features was difficult and left only to "experts" or "professionals". featurewiz was created to help you in this endeavor.
-

featurewiz now enables you to add hundreds of such features with a single line of code. Set the "feature_engg" flag to "interactions", "groupby" or "target" and featurewiz will select the best encoders for each of those options and create hundreds (perhaps thousands) of features in one go. Not only that, using the next step, featurewiz will sift through numerous such variables and find only the least correlated and most relevant features to your model. All in one step!.
+## Internals +### featurewiz - Transform Your Data Science Workflow using two modules: +#### 1. Feature Engineering Module +

  • Advanced Feature Creation: use Deep Learning based Auto Encoders and GAN's to extract features to add to your data. These powerful capabilities will help you in solving your toughest problems.
  • +
  • Options for Enhancement: Use "interactions", "groupby", or "target" flags to enable advanced feature engineering techniques.
  • +
  • Kaggle-Ready: Designed to meet the high standards of feature engineering required in competitive data science, like Kaggle.
  • +
  • Efficient and User-Friendly: Generate and sift through thousands of features, selecting only the most impactful ones for your model.
  • -![feature_engg](images/feature_engg.jpg) +![feature_engg](images/feature_engg.png) -### 2. Feature Selection -

    The second step is Feature Selection. `featurewiz` uses the MRMR (Minimum Redundancy Maximum Relevance) algorithm as the basis for its feature selection.
    - Why perform Feature Selection? Once you have created 100's of new features, you still have three questions left to answer: -1. How do we interpret those newly created features? -2. Which of these features is important and which is useless? How many of them are highly correlated to each other causing redundancy? -3. Does the model overfit now on these new features and perform better or worse than before? -
    -All are very important questions and featurewiz answers them by using the SULOV method and Recursive XGBoost to reduce features in your dataset to the best "minimum optimal" features for the model.
    -

    SULOV: SULOV stands for `Searching for Uncorrelated List of Variables`. The SULOV algorithm is based on the Minimum-Redundancy-Maximum-Relevance (MRMR) algorithm explained in this article as one of the best feature selection methods. To understand how MRMR works and how it is different from `Boruta` and other feature selection methods, see the chart below. Here "Minimal Optimal" refers to MRMR (featurewiz) while "all-relevant" refers to Boruta.
    +#### 2. Feature Selection Module +

  • MRMR Algorithm: Employs Minimum Redundancy Maximum Relevance (MRMR) for effective feature selection.
  • +
  • SULOV Method: Stands for 'Searching for Uncorrelated List of Variables', ensuring low redundancy and high relevance in feature selection.
  • +
  • Addressing Key Questions: Helps interpret new features, assess their importance, and evaluate the model's performance with these features.
  • +
  • Optimal Feature Subset: Uses Recursive XGBoost in combination with SULOV to identify the most critical features, reducing overfitting and improving model interpretability.
  • + +#### Chart Comparison: +Minimal Optimal (MRMR - featurewiz) vs. All-Relevant (Boruta): Understand how featurewiz's MRMR approach differs from other methods like Boruta for comprehensive feature selection. The SULOV algorithm is based on the Minimum-Redundancy-Maximum-Relevance (MRMR) algorithm explained in this article as one of the best feature selection methods. -![MRMR_chart](images/MRMR.png) +![Learn More About MRMR](images/MRMR.png) + +Transform your feature engineering and selection process with featurewiz - the tool that brings expert-level capabilities to your fingertips! ## Working `featurewiz` performs feature selection in 2 steps. Each step is explained below. @@ -88,7 +114,7 @@ Here are some additional tips for ML engineers and data scientists when using fe
    1. How to cross-validate your results: When you use featurewiz, we automatically perform multiple rounds of feature selection using permutations on the number of columns. However, you can perform feature selection using permutations of rows as follows in cross_validate using featurewiz.
    2. Use multiple feature selection tools: It is a good idea to use multiple feature selection tools and compare the results. This will help you to get a better understanding of which features are most important for your data.
    3. -
    4. Don't forget to engineer new features: Feature selection is only one part of the process of building a good machine learning model. You should also spend time engineering your features to make them as informative as possible. This can involve things like creating new features, transforming existing features, and removing irrelevant features.
    5. +
    6. Don't forget to use Auto Encoders!: Autoencoders are like skilled artists who can draw a quick sketch of a complex picture. They learn to capture the essence of the data and then recreate it with as few strokes as possible. This process helps in understanding and compressing data efficiently.
    7. Don't overfit your model: It is important to avoid overfitting your model to the training data. Overfitting occurs when your model learns the noise in the training data, rather than the underlying signal. To avoid overfitting, you can use regularization techniques, such as lasso or elasticnet.
    8. Start with a small number of features: When you are first starting out, it is a good idea to start with a small number of features. This will help you to avoid overfitting your model. As you become more experienced, you can experiment with adding more features.
    @@ -135,7 +161,8 @@ There are two ways to use featurewiz. ``` from featurewiz import FeatureWiz fwiz = FeatureWiz(feature_engg = '', nrows=None, transform_target=True, scalers="std", - category_encoders="auto", add_missing=False, verbose=0) + category_encoders="auto", add_missing=False, verbose=0, imbalanced=False, + ae_options={}) X_train_selected, y_train = fwiz.fit_transform(X_train, y_train) X_test_selected = fwiz.transform(X_test) ### get list of selected features ### @@ -175,7 +202,13 @@ You don't have to tell Featurewiz whether it is a Regression or Classification p feature_engg : str or list, default='' Specifies the feature engineering methods to apply, such as 'interactions', 'groupby', - and 'target'. + and 'target'. +Update: Five new options have been added recently to `feature_engg` (starting in version 0.5.0): `dae`, `vae`, `dae_add`, `vae_add` and `gan`. These are auto encoders that can extract the most important patterns in your data and add them as extra features to your data using neural networks. Try them for your toughest ML problems! + + ae_options : dict, default={} + You can provide a dictionary for tuning auto encoders above. Supported auto encoders include 'dae', + 'vae', and 'gan'. You must use the help function to see how to send a dict to each auto encoder. You can also check out this example: +[Auto Encoder demo notebook](https://github.com/AutoViML/featurewiz/blob/main/examples/Featurewiz_with_AutoEncoder_Demo.ipynb) category_encoders : str or list, default='' Encoders for handling categorical variables. Supported encoders include 'onehot', @@ -206,6 +239,9 @@ You don't have to tell Featurewiz whether it is a Regression or Classification p Specifies the scaler to use for feature scaling. Available options include 'std', 'standard', 'minmax', 'max', 'robust', 'maxabs'. + imbalanced : True or False, default=False + Specifies whether to use SMOTE technique for imbalanced datasets. + **Input Arguments for old syntax** - `dataname`: could be a datapath+filename or a dataframe. It will detect whether your input is a filename or a dataframe and load it automatically. @@ -265,7 +301,7 @@ In most cases, featurewiz builds models with 20%-99% fewer features than your or

    featurewiz is every Data Scientist's feature wizard that will:

    1. Automatically pre-process data: you can send in your entire dataframe "as is" and featurewiz will classify and change/label encode categorical variables changes to help XGBoost processing. It classifies variables as numeric or categorical or NLP or date-time variables automatically so it can use them correctly to model.
      -
    2. Perform feature engineering automatically: The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables is difficult and sifting through those hundreds of new features is painstaking and left only to "experts". Now, with featurewiz you can create hundreds or even thousands of new features with the click of a mouse. This is very helpful when you have a small number of features to start with. However, be careful with this option. You can very easily create a monster with this option. +
    3. Perform feature engineering automatically: The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables is difficult and sifting through those hundreds of new features is painstaking and left only to "experts". Now, with featurewiz you can use deep learning to extract features with the click of a mouse. This is very helpful when you have imbalanced classes or 1000's of features to deal with. However, be careful with this option. You can very easily spend a lot of time tuning these neural networks.
    4. Perform feature reduction automatically. When you have small data sets and you know your domain well, it is easy to perhaps do EDA and identify which variables are important. But when you have a very large data set with hundreds if not thousands of variables, selecting the best features from your model can mean the difference between a bloated and highly complex model or a simple model with the fewest and most information-rich features. featurewiz uses XGBoost repeatedly to perform feature selection. You must try it on your large data sets and compare!
    5. Explain SULOV method graphically using networkx library so you can see which variables are highly correlated to which ones and which of those have high or low mutual information scores automatically. Just set verbose = 2 to see the graph.
    6. Build a fast XGBoost or LightGBM model using the features selected by featurewiz. There is a function called "simple_lightgbm_model" which you can use to build a fast model. It is a new module, so check it out.
      diff --git a/build/lib/featurewiz/__init__.py b/build/lib/featurewiz/__init__.py new file mode 100644 index 0000000..77385bc --- /dev/null +++ b/build/lib/featurewiz/__init__.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +################################################################################ +# featurewiz - advanced feature engineering and best features selection in single line of code +# Python v3.6+ +# Created by Ram Seshadri +# Licensed under Apache License v2 +################################################################################ +# Version +from .__version__ import __version__ +from .featurewiz import featurewiz +from .featurewiz import FE_split_one_field_into_many, FE_add_groupby_features_aggregated_to_dataframe +from .featurewiz import FE_start_end_date_time_features +from .featurewiz import classify_features +from .featurewiz import classify_columns,FE_combine_rare_categories +from .featurewiz import FE_count_rows_for_all_columns_by_group +from .featurewiz import FE_add_age_by_date_col, FE_split_add_column, FE_get_latest_values_based_on_date_column +from .featurewiz import FE_capping_outliers_beyond_IQR_Range +from .featurewiz import EDA_classify_and_return_cols_by_type, EDA_classify_features_for_deep_learning +from .featurewiz import FE_create_categorical_feature_crosses, EDA_find_skewed_variables +from .featurewiz import FE_kmeans_resampler, FE_find_and_cap_outliers, EDA_find_outliers +from .featurewiz import split_data_n_ways, FE_concatenate_multiple_columns +from .featurewiz import FE_discretize_numeric_variables, reduce_mem_usage +from .ml_models import simple_XGBoost_model, simple_LightGBM_model, complex_XGBoost_model +from .ml_models import complex_LightGBM_model,data_transform, get_class_weights +from .my_encoders import My_LabelEncoder, Groupby_Aggregator, My_LabelEncoder_Pipe, Ranking_Aggregator, DateTime_Transformer +from .my_encoders import Rare_Class_Combiner, Rare_Class_Combiner_Pipe, FE_create_time_series_features, Binning_Transformer +from .my_encoders import Column_Names_Transformer, FE_convert_all_object_columns_to_numeric, Numeric_Transformer +from .my_encoders import TS_Lagging_Transformer, TS_Fourier_Transformer, TS_Trend_Seasonality_Transformer +from .my_encoders import TS_Lagging_Transformer_Pipe, TS_Fourier_Transformer_Pipe +from lazytransform import LazyTransformer, SuloRegressor, SuloClassifier, print_regression_metrics, print_classification_metrics +from lazytransform import print_regression_model_stats, YTransformer, print_sulo_accuracy +from .sulov_method import FE_remove_variables_using_SULOV_method +from .featurewiz import FE_transform_numeric_columns_to_bins, FE_create_interaction_vars +from .stacking_models import Stacking_Classifier, Blending_Regressor, Stacking_Regressor, stacking_models_list +from .stacking_models import StackingClassifier_Multi, analyze_problem_type_array +from .stacking_models import DenoisingAutoEncoder, VariationalAutoEncoder +from .stacking_models import GAN, GANAugmenter +from .featurewiz import EDA_binning_numeric_column_displaying_bins, FE_calculate_duration_from_timestamp +from .featurewiz import FE_convert_mixed_datatypes_to_string, FE_drop_rows_with_infinity +from .featurewiz import EDA_find_remove_columns_with_infinity, FE_split_list_into_columns +from .featurewiz import EDA_remove_special_chars, FE_remove_commas_in_numerics +from .featurewiz import EDA_randomly_select_rows_from_dataframe, remove_duplicate_cols_in_dataset +from .featurewiz import cross_val_model_predictions, get_class_distribution +from .featurewiz import FeatureWiz +################################################################################ +if __name__ == "__main__": + module_type = 'Running' +else: + module_type = 'Imported' +version_number = __version__ +print("""%s featurewiz %s. Use the following syntax: + >>> wiz = FeatureWiz(feature_engg = '', nrows=None, transform_target=True, scalers="std", + category_encoders="auto", add_missing=False, verbose=0. imbalanced=False, + ae_options={}) + >>> X_train_selected, y_train = wiz.fit_transform(X_train, y_train) + >>> X_test_selected = wiz.transform(X_test) + >>> selected_features = wiz.features + """ %(module_type, version_number)) +################################################################################ diff --git a/build/lib/featurewiz/__version__.py b/build/lib/featurewiz/__version__.py new file mode 100644 index 0000000..546ea9c --- /dev/null +++ b/build/lib/featurewiz/__version__.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +"""Specifies the version of the featurewiz package.""" + +__title__ = "featurewiz" +__author__ = "Ram Seshadri" +__description__ = "Advanced Feature Engineering and Feature Selection for any data set, any size" +__url__ = "https://github.com/Auto_ViML/featurewiz.git" +__version__ = "0.5.0" +__license__ = "Apache License 2.0" +__copyright__ = "2020-23 Google" diff --git a/build/lib/featurewiz/classify_method.py b/build/lib/featurewiz/classify_method.py new file mode 100644 index 0000000..f94f8af --- /dev/null +++ b/build/lib/featurewiz/classify_method.py @@ -0,0 +1,320 @@ +import numpy as np +import pandas as pd +import random +np.random.seed(99) +random.seed(42) +################################################################################ +#### The warnings from Sklearn are so annoying that I have to shut it off ####### +import warnings +warnings.filterwarnings("ignore") +from sklearn.exceptions import DataConversionWarning +warnings.filterwarnings(action='ignore', category=DataConversionWarning) +def warn(*args, **kwargs): + pass +warnings.warn = warn +import logging +#################################################################################### +import pdb +from functools import reduce +import copy +import time +################################################################################# +def left_subtract(l1,l2): + lst = [] + for i in l1: + if i not in l2: + lst.append(i) + return lst +################################################################################# +import copy +def EDA_find_remove_columns_with_infinity(df, remove=False): + """ + This function finds all columns in a dataframe that have inifinite values (np.inf or -np.inf) + It returns a list of column names. If the list is empty, it means no columns were found. + If remove flag is set, then it returns a smaller dataframe with inf columns removed. + """ + nums = df.select_dtypes(include='number').columns.tolist() + dfx = df[nums] + sum_rows = np.isinf(dfx).values.sum() + add_cols = list(dfx.columns.to_series()[np.isinf(dfx).any()]) + if sum_rows > 0: + print(' there are %d rows and %d columns with infinity in them...' %(sum_rows,len(add_cols))) + if remove: + ### here you need to use df since the whole dataset is involved ### + nocols = [x for x in df.columns if x not in add_cols] + print(" Shape of dataset before %s and after %s removing columns with infinity" %(df.shape,(df[nocols].shape,))) + return df[nocols] + else: + ## this will be a list of columns with infinity #### + return add_cols + else: + ## this will be an empty list if there are no columns with infinity + return add_cols +#################################################################################### +def classify_columns(df_preds, verbose=0): + """ + This actually does Exploratory data analysis - it means this function performs EDA + ###################################################################################### + Takes a dataframe containing only predictors to be classified into various types. + DO NOT SEND IN A TARGET COLUMN since it will try to include that into various columns. + Returns a data frame containing columns and the class it belongs to such as numeric, + categorical, date or id column, boolean, nlp, discrete_string and cols to delete... + ####### Returns a dictionary with 10 kinds of vars like the following: # continuous_vars,int_vars + # cat_vars,factor_vars, bool_vars,discrete_string_vars,nlp_vars,date_vars,id_vars,cols_delete + """ + train = copy.deepcopy(df_preds) + #### If there are 30 chars are more in a discrete_string_var, it is then considered an NLP variable + max_nlp_char_size = 30 + max_cols_to_print = 30 + print('#######################################################################################') + print('######################## C L A S S I F Y I N G V A R I A B L E S ####################') + print('#######################################################################################') + if verbose: + print('Classifying variables in data set...') + #### Cat_Limit defines the max number of categories a column can have to be called a categorical colum + cat_limit = 35 + float_limit = 15 #### Make this limit low so that float variables below this limit become cat vars ### + def add(a,b): + return a+b + sum_all_cols = dict() + orig_cols_total = train.shape[1] + #Types of columns + cols_delete = [] + cols_delete = [col for col in list(train) if (len(train[col].value_counts()) == 1 + ) | (train[col].isnull().sum()/len(train) >= 0.90)] + inf_cols = EDA_find_remove_columns_with_infinity(train) + mixed_cols = [x for x in list(train) if len(train[x].dropna().apply(type).value_counts()) > 1] + if len(mixed_cols) > 0: + print(' Removing %s column(s) due to mixed data type detected...' %mixed_cols) + cols_delete += mixed_cols + cols_delete += inf_cols + train = train[left_subtract(list(train),cols_delete)] + var_df = pd.Series(dict(train.dtypes)).reset_index(drop=False).rename( + columns={0:'type_of_column'}) + sum_all_cols['cols_delete'] = cols_delete + + var_df['bool'] = var_df.apply(lambda x: 1 if x['type_of_column'] in ['bool','object'] + and len(train[x['index']].value_counts()) == 2 else 0, axis=1) + string_bool_vars = list(var_df[(var_df['bool'] ==1)]['index']) + sum_all_cols['string_bool_vars'] = string_bool_vars + var_df['num_bool'] = var_df.apply(lambda x: 1 if x['type_of_column'] in [np.uint8, + np.uint16, np.uint32, np.uint64, + 'int8','int16','int32','int64', + 'float16','float32','float64'] and len( + train[x['index']].value_counts()) == 2 else 0, axis=1) + num_bool_vars = list(var_df[(var_df['num_bool'] ==1)]['index']) + sum_all_cols['num_bool_vars'] = num_bool_vars + ###### This is where we take all Object vars and split them into diff kinds ### + discrete_or_nlp = var_df.apply(lambda x: 1 if x['type_of_column'] in ['object'] and x[ + 'index'] not in string_bool_vars+cols_delete else 0,axis=1) + ######### This is where we figure out whether a string var is nlp or discrete_string var ### + var_df['nlp_strings'] = 0 + var_df['discrete_strings'] = 0 + var_df['cat'] = 0 + var_df['id_col'] = 0 + discrete_or_nlp_vars = var_df.loc[discrete_or_nlp==1]['index'].values.tolist() + copy_discrete_or_nlp_vars = copy.deepcopy(discrete_or_nlp_vars) + if len(discrete_or_nlp_vars) > 0: + for col in copy_discrete_or_nlp_vars: + #### first fill empty or missing vals since it will blowup ### + ### Remember that fillna only works at the dataframe level! + train[[col]] = train[[col]].fillna(' ') + if train[col].map(lambda x: len(x) if type(x)==str else 0).max( + ) >= 50 and len(train[col].value_counts() + ) >= int(0.9*len(train)) and col not in string_bool_vars: + var_df.loc[var_df['index']==col,'nlp_strings'] = 1 + elif train[col].map(lambda x: len(x) if type(x)==str else 0).mean( + ) >= max_nlp_char_size and train[col].map(lambda x: len(x) if type(x)==str else 0).max( + ) < 50 and len(train[col].value_counts() + ) <= int(0.9*len(train)) and col not in string_bool_vars: + var_df.loc[var_df['index']==col,'discrete_strings'] = 1 + elif len(train[col].value_counts()) > cat_limit and len(train[col].value_counts() + ) <= int(0.9*len(train)) and col not in string_bool_vars: + var_df.loc[var_df['index']==col,'discrete_strings'] = 1 + elif len(train[col].value_counts()) > cat_limit and len(train[col].value_counts() + ) == len(train) and col not in string_bool_vars: + var_df.loc[var_df['index']==col,'id_col'] = 1 + else: + var_df.loc[var_df['index']==col,'cat'] = 1 + nlp_vars = list(var_df[(var_df['nlp_strings'] ==1)]['index']) + sum_all_cols['nlp_vars'] = nlp_vars + discrete_string_vars = list(var_df[(var_df['discrete_strings'] ==1) ]['index']) + sum_all_cols['discrete_string_vars'] = discrete_string_vars + ###### This happens only if a string column happens to be an ID column ####### + #### DO NOT Add this to ID_VARS yet. It will be done later.. Dont change it easily... + #### Category DTYPE vars are very special = they can be left as is and not disturbed in Python. ### + var_df['dcat'] = var_df.apply(lambda x: 1 if str(x['type_of_column'])=='category' else 0, + axis=1) + factor_vars = list(var_df[(var_df['dcat'] ==1)]['index']) + sum_all_cols['factor_vars'] = factor_vars + ######################################################################## + date_or_id = var_df.apply(lambda x: 1 if x['type_of_column'] in [np.uint8, + np.uint16, np.uint32, np.uint64, + 'int8','int16', + 'int32','int64'] and x[ + 'index'] not in string_bool_vars+num_bool_vars+discrete_string_vars+nlp_vars else 0, + axis=1) + ######### This is where we figure out whether a numeric col is date or id variable ### + var_df['int'] = 0 + var_df['date_time'] = 0 + ### if a particular column is date-time type, now set it as a date time variable ## + var_df['date_time'] = var_df.apply(lambda x: 1 if x['type_of_column'] in [' 2050: + var_df.loc[var_df['index']==col,'id_col'] = 1 + else: + try: + pd.to_datetime(train[col],infer_datetime_format=True) + var_df.loc[var_df['index']==col,'date_time'] = 1 + except: + var_df.loc[var_df['index']==col,'id_col'] = 1 + else: + if train[col].min() < 1900 or train[col].max() > 2050: + if col not in num_bool_vars: + var_df.loc[var_df['index']==col,'int'] = 1 + else: + try: + pd.to_datetime(train[col],infer_datetime_format=True) + var_df.loc[var_df['index']==col,'date_time'] = 1 + except: + if col not in num_bool_vars: + var_df.loc[var_df['index']==col,'int'] = 1 + else: + pass + int_vars = list(var_df[(var_df['int'] ==1)]['index']) + date_vars = list(var_df[(var_df['date_time'] == 1)]['index']) + id_vars = list(var_df[(var_df['id_col'] == 1)]['index']) + sum_all_cols['int_vars'] = int_vars + copy_date_vars = copy.deepcopy(date_vars) + for date_var in copy_date_vars: + #### This test is to make sure sure date vars are actually date vars + try: + pd.to_datetime(train[date_var],infer_datetime_format=True) + except: + ##### if not a date var, then just add it to delete it from processing + cols_delete.append(date_var) + date_vars.remove(date_var) + sum_all_cols['date_vars'] = date_vars + sum_all_cols['id_vars'] = id_vars + sum_all_cols['cols_delete'] = cols_delete + ## This is an EXTREMELY complicated logic for cat vars. Don't change it unless you test it many times! + var_df['numeric'] = 0 + float_or_cat = var_df.apply(lambda x: 1 if x['type_of_column'] in ['float16', + 'float32','float64'] else 0, + axis=1) + ####### We need to make sure there are no categorical vars in float ####### + if len(var_df.loc[float_or_cat == 1]) > 0: + for col in var_df.loc[float_or_cat == 1]['index'].values.tolist(): + if len(train[col].value_counts()) > 2 and len(train[col].value_counts() + ) <= float_limit and len(train[col].value_counts()) <= len(train): + var_df.loc[var_df['index']==col,'cat'] = 1 + else: + if col not in (num_bool_vars + factor_vars): + var_df.loc[var_df['index']==col,'numeric'] = 1 + cat_vars = list(var_df[(var_df['cat'] ==1)]['index']) + continuous_vars = list(var_df[(var_df['numeric'] ==1)]['index']) + + ######## V E R Y I M P O R T A N T ################################################### + cat_vars_copy = copy.deepcopy(factor_vars) + for cat in cat_vars_copy: + if df_preds[cat].dtype==float: + continuous_vars.append(cat) + factor_vars.remove(cat) + var_df.loc[var_df['index']==cat,'dcat'] = 0 + var_df.loc[var_df['index']==cat,'numeric'] = 1 + elif len(df_preds[cat].value_counts()) == df_preds.shape[0]: + id_vars.append(cat) + factor_vars.remove(cat) + var_df.loc[var_df['index']==cat,'dcat'] = 0 + var_df.loc[var_df['index']==cat,'id_col'] = 1 + + sum_all_cols['factor_vars'] = factor_vars + ##### There are a couple of extra tests you need to do to remove abberations in cat_vars ### + cat_vars_copy = copy.deepcopy(cat_vars) + for cat in cat_vars_copy: + if df_preds[cat].dtype==float: + continuous_vars.append(cat) + cat_vars.remove(cat) + var_df.loc[var_df['index']==cat,'cat'] = 0 + var_df.loc[var_df['index']==cat,'numeric'] = 1 + elif len(df_preds[cat].value_counts()) == df_preds.shape[0]: + id_vars.append(cat) + cat_vars.remove(cat) + var_df.loc[var_df['index']==cat,'cat'] = 0 + var_df.loc[var_df['index']==cat,'id_col'] = 1 + sum_all_cols['cat_vars'] = cat_vars + sum_all_cols['continuous_vars'] = continuous_vars + sum_all_cols['id_vars'] = id_vars + ###### This is where you consoldate the numbers ########### + var_dict_sum = dict(zip(var_df.values[:,0], var_df.values[:,2:].sum(1))) + for col, sumval in var_dict_sum.items(): + if sumval == 0: + print('%s of type=%s is not classified' %(col,train[col].dtype)) + elif sumval > 1: + print('%s of type=%s is classified into more then one type' %(col,train[col].dtype)) + else: + pass + ##### If there are more than 1000 unique values, then add it to NLP vars ### + copy_discretes = copy.deepcopy(discrete_string_vars) + for each_discrete in copy_discretes: + if train[each_discrete].nunique() >= 1000: + nlp_vars.append(each_discrete) + discrete_string_vars.remove(each_discrete) + elif train[each_discrete].nunique() > 100 and train[each_discrete].nunique() < 1000: + pass + else: + ### If it is less than 100 unique values, then make it categorical var + cat_vars.append(each_discrete) + discrete_string_vars.remove(each_discrete) + sum_all_cols['discrete_string_vars'] = discrete_string_vars + sum_all_cols['cat_vars'] = cat_vars + sum_all_cols['nlp_vars'] = nlp_vars + ############### This is where you print all the types of variables ############## + ####### Returns 8 vars in the following order: continuous_vars,int_vars,cat_vars, + ### string_bool_vars,discrete_string_vars,nlp_vars,date_or_id_vars,cols_delete + if verbose == 1: + print(" Number of Numeric Columns = ", len(continuous_vars)) + print(" Number of Integer-Categorical Columns = ", len(int_vars)) + print(" Number of String-Categorical Columns = ", len(cat_vars)) + print(" Number of Factor-Categorical Columns = ", len(factor_vars)) + print(" Number of String-Boolean Columns = ", len(string_bool_vars)) + print(" Number of Numeric-Boolean Columns = ", len(num_bool_vars)) + print(" Number of Discrete String Columns = ", len(discrete_string_vars)) + print(" Number of NLP String Columns = ", len(nlp_vars)) + print(" Number of Date Time Columns = ", len(date_vars)) + print(" Number of ID Columns = ", len(id_vars)) + print(" Number of Columns to Delete = ", len(cols_delete)) + if verbose == 2: + print(' Printing upto %d columns max in each category:' %max_cols_to_print) + print(" Numeric Columns : %s" %continuous_vars[:max_cols_to_print]) + print(" Integer-Categorical Columns: %s" %int_vars[:max_cols_to_print]) + print(" String-Categorical Columns: %s" %cat_vars[:max_cols_to_print]) + print(" Factor-Categorical Columns: %s" %factor_vars[:max_cols_to_print]) + print(" String-Boolean Columns: %s" %string_bool_vars[:max_cols_to_print]) + print(" Numeric-Boolean Columns: %s" %num_bool_vars[:max_cols_to_print]) + print(" Discrete String Columns: %s" %discrete_string_vars[:max_cols_to_print]) + print(" NLP text Columns: %s" %nlp_vars[:max_cols_to_print]) + print(" Date Time Columns: %s" %date_vars[:max_cols_to_print]) + print(" ID Columns: %s" %id_vars[:max_cols_to_print]) + print(" Columns that will not be considered in modeling: %s" %cols_delete[:max_cols_to_print]) + ##### now collect all the column types and column names into a single dictionary to return! + + len_sum_all_cols = reduce(add,[len(v) for v in sum_all_cols.values()]) + if len_sum_all_cols == orig_cols_total: + if verbose: + print(' %d Predictors classified...' %orig_cols_total) + #print(' This does not include the Target column(s)') + else: + print('No of columns classified %d does not match %d total cols. Continuing...' %( + len_sum_all_cols, orig_cols_total)) + ls = sum_all_cols.values() + flat_list = [item for sublist in ls for item in sublist] + if len(left_subtract(list(train),flat_list)) > 0: + print(' Error: some columns missing from classification are: %s' %left_subtract(list(train),flat_list)) + return sum_all_cols +#################################################################################### diff --git a/build/lib/featurewiz/databunch.py b/build/lib/featurewiz/databunch.py new file mode 100644 index 0000000..5978325 --- /dev/null +++ b/build/lib/featurewiz/databunch.py @@ -0,0 +1,686 @@ +############################################################################### +# MIT License +# +# Copyright (c) 2020 Alex Lekov +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### +##### This amazing Library was created by Alex Lekov: Many Thanks to Alex! ### +##### https://github.com/Alex-Lekov/AutoML_Alex ### +############################################################################### +import pandas as pd +import numpy as np +from itertools import combinations +from sklearn.preprocessing import StandardScaler +from category_encoders import HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder +from category_encoders import OneHotEncoder, HelmertEncoder, OrdinalEncoder, CountEncoder, BaseNEncoder +from category_encoders import TargetEncoder, CatBoostEncoder, WOEEncoder, JamesSteinEncoder +from category_encoders.glmm import GLMMEncoder +from sklearn.preprocessing import LabelEncoder +from category_encoders.wrapper import PolynomialWrapper +from .encoders import FrequencyEncoder +from . import settings + +import pdb +# disable chained assignments +pd.options.mode.chained_assignment = None +import copy +import dask +import dask.dataframe as dd + +class DataBunch(object): + """ + Сlass for storing, cleaning and processing your dataset + """ + def __init__(self, + X_train=None, + y_train=None, + X_test=None, + y_test=None, + cat_features=None, + clean_and_encod_data=True, + cat_encoder_names=None, + clean_nan=True, + num_generator_features=True, + group_generator_features=True, + target_enc_cat_features=True, + normalization=True, + random_state=42, + verbose=1): + """ + Description of __init__ + + Args: + X_train=None (undefined): dataset + y_train=None (undefined): y + X_test=None (undefined): dataset + y_test=None (undefined): y + cat_features=None (list or None): + clean_and_encod_data=True (undefined): + cat_encoder_names=None (list or None): + clean_nan=True (undefined): + num_generator_features=True (undefined): + group_generator_features=True (undefined): + target_enc_cat_features=True (undefined) + random_state=42 (undefined): + verbose = 1 (undefined) + """ + self.random_state = random_state + + self.X_train = None + self.y_train = None + self.X_test = None + self.y_test = None + self.X_train_predicts = None + self.X_test_predicts = None + self.cat_features = None + + # Encoders + self.cat_encoders_names = settings.cat_encoders_names + self.target_encoders_names = settings.target_encoders_names + + + self.cat_encoder_names = cat_encoder_names + self.cat_encoder_names_list = list(self.cat_encoders_names.keys()) + list(self.target_encoders_names.keys()) + self.target_encoders_names_list = list(self.target_encoders_names.keys()) + + # check X_train, y_train, X_test + if self.check_data_format(X_train): + if type(X_train) == dask.dataframe.core.DataFrame: + self.X_train_source = X_train.compute() + else: + self.X_train_source = pd.DataFrame(X_train) + self.X_train_source = remove_duplicate_cols_in_dataset(self.X_train_source) + if X_test is not None: + if self.check_data_format(X_test): + if type(X_test) == dask.dataframe.core.DataFrame: + self.X_test_source = X_test.compute() + else: + self.X_test_source = pd.DataFrame(X_test) + self.X_test_source = remove_duplicate_cols_in_dataset(self.X_test_source) + + + ### There is a chance for an error in this - so worth watching! + if y_train is not None: + le = LabelEncoder() + if self.check_data_format(y_train): + if settings.multi_label: + ### if the model is mult-Label, don't transform it since it won't work + self.y_train_source = y_train + else: + if not isinstance(y_train, pd.DataFrame): + if y_train.dtype == 'object' or str(y_train.dtype) == 'category': + self.y_train_source = le.fit_transform(y_train) + else: + if settings.modeltype == 'Multi_Classification': + rare_class = find_rare_class(y_train) + if rare_class != 0: + ### if the rare class is not zero, then transform it using Label Encoder + y_train = le.fit_transform(y_train) + self.y_train_source = copy.deepcopy(y_train) + else: + print('Error: y_train should be a series. Skipping target encoding for dataset...') + target_enc_cat_features = False + else: + if settings.multi_label: + self.y_train_source = pd.DataFrame(y_train) + else: + if y_train.dtype == 'object' or str(y_train.dtype) == 'category': + self.y_train_source = le.fit_transform(pd.DataFrame(y_train)) + else: + self.y_train_source = copy.deepcopy(y_train) + else: + print("No target data found!") + return + + if y_test is not None: + self.y_test = y_test + + if verbose > 0: + print('Source X_train shape: ', self.X_train_source.shape) + if not X_test is None: + print('| Source X_test shape: ', self.X_test_source.shape) + print('#'*50) + + # add categorical features in DataBunch + if cat_features is None: + self.cat_features = self.auto_detect_cat_features(self.X_train_source) + if verbose > 0: + print('Auto detect cat features: ', len(self.cat_features)) + + else: + self.cat_features = list(cat_features) + + # preproc_data in DataBunch + if clean_and_encod_data: + if verbose > 0: + print('> Start preprocessing with %d variables' %self.X_train_source.shape[1]) + self.X_train, self.X_test = self.preproc_data(self.X_train_source, + self.X_test_source, + self.y_train_source, + cat_features=self.cat_features, + cat_encoder_names=cat_encoder_names, + clean_nan=clean_nan, + num_generator_features=num_generator_features, + group_generator_features=group_generator_features, + target_enc_cat_features=target_enc_cat_features, + normalization=normalization, + verbose=verbose,) + else: + self.X_train, self.X_test = X_train, X_test + + + def check_data_format(self, data): + """ + Description of check_data_format: + Check that data is not pd.DataFrame or empty + + Args: + data (undefined): dataset + Return: + True or Exception + """ + data_tmp = pd.DataFrame(data) + if data_tmp is None or data_tmp.empty: + raise Exception("data is not pd.DataFrame or empty") + else: + if isinstance(data, pd.Series) or isinstance(data, pd.DataFrame): + return True + elif isinstance(data, np.ndarray): + return True + elif type(data) == dask.dataframe.core.DataFrame: + return True + else: + False + + def clean_nans(self, data, cols=None): + """ + Fill Nans and add column, that there were nans in this column + + Args: + data (pd.DataFrame, shape (n_samples, n_features)): the input data + cols list() features: the input data + Return: + Clean data (pd.DataFrame, shape (n_samples, n_features)) + + """ + if cols is not None: + nan_columns = list(data[cols].columns[data[cols].isnull().sum() > 0]) + if nan_columns: + for nan_column in nan_columns: + data[nan_column+'_isNAN'] = pd.isna(data[nan_column]).astype('uint8') + data.fillna(data.median(), inplace=True) + return(data) + + + def auto_detect_cat_features(self, data): + """ + Description of _auto_detect_cat_features: + Auto-detection categorical_features by simple rule: + categorical feature == if feature nunique low 1% of data + + Args: + data (pd.DataFrame): dataset + + Returns: + cat_features (list): columns names cat features + + """ + #object_features = list(data.columns[data.dtypes == 'object']) + cat_features = data.columns[(data.nunique(dropna=False) < len(data)//100) & \ + (data.nunique(dropna=False) >2)] + #cat_features = list(set([*object_features, *cat_features])) + return (cat_features) + + + def gen_cat_encodet_features(self, data, cat_encoder_name): + """ + Description of _encode_features: + Encode car features + + Args: + data (pd.DataFrame): + cat_encoder_name (str): cat Encoder name + + Returns: + pd.DataFrame + + """ + + if isinstance(cat_encoder_name, str): + if cat_encoder_name in self.cat_encoder_names_list and cat_encoder_name not in self.target_encoders_names_list: + if cat_encoder_name == 'HashingEncoder': + encoder = self.cat_encoders_names[cat_encoder_name][0](cols=self.cat_features, n_components=int(np.log(len(data.columns))*1000), + drop_invariant=True) + else: + encoder = self.cat_encoders_names[cat_encoder_name][0](cols=self.cat_features, drop_invariant=True) + data_encodet = encoder.fit_transform(data) + data_encodet = data_encodet.add_prefix(cat_encoder_name + '_') + else: + print(f"{cat_encoder_name} is not supported!") + return ('', '') + else: + encoder = copy.deepcopy(cat_encoder_name) + data_encodet = encoder.transform(data) + data_encodet = data_encodet.add_prefix(str(cat_encoder_name).split("(")[0] + '_') + + + return (data_encodet, encoder) + + + def gen_target_encodet_features(self, x_data, y_data=None, cat_encoder_name=''): + """ + Description of _encode_features: + Encode car features + + Args: + data (pd.DataFrame): + cat_encoder_name (str): cat Encoder name + + Returns: + pd.DataFrame + + """ + + + if isinstance(cat_encoder_name, str): + ### If it is the first time, it will perform fit_transform ! + if cat_encoder_name in self.target_encoders_names_list: + encoder = self.target_encoders_names[cat_encoder_name][0](cols=self.cat_features, drop_invariant=True) + if settings.modeltype == 'Multi_Classification': + ### you must put a Polynomial Wrapper on the cat_encoder in case the model is multi-class + if cat_encoder_name in ['WOEEncoder']: + encoder = PolynomialWrapper(encoder) + ### All other encoders TargetEncoder CatBoostEncoder GLMMEncoder don't need + ### Polynomial Wrappers since they handle multi-class (label encoded) very well! + cols = encoder.cols + for each_col in cols: + x_data[each_col] = encoder.fit_transform(x_data[each_col], y_data).values + data_encodet = encoder.fit_transform(x_data, y_data) + data_encodet = data_encodet.add_prefix(cat_encoder_name + '_') + else: + print(f"{cat_encoder_name} is not supported!") + return ('', '') + else: + ### if it is already fit, then it will only do transform here ! + encoder = copy.deepcopy(cat_encoder_name) + data_encodet = encoder.transform(x_data) + data_encodet = data_encodet.add_prefix(str(cat_encoder_name).split("(")[0] + '_') + + + return (data_encodet, encoder) + + def gen_numeric_interaction_features(self, + df, + columns, + operations=['/','*','-','+'],): + """ + Description of numeric_interaction_terms: + Numerical interaction generator features: A/B, A*B, A-B, + + Args: + df (pd.DataFrame): + columns (list): num columns names + operations (list): operations type + + Returns: + pd.DataFrame + + """ + copy_columns = copy.deepcopy(columns) + fe_df = pd.DataFrame() + for combo_col in combinations(columns,2): + if '/' in operations: + fe_df['{}_div_by_{}'.format(combo_col[0], combo_col[1]) ] = (df[combo_col[0]]*1.) / df[combo_col[1]] + if '*' in operations: + fe_df['{}_mult_by_{}'.format(combo_col[0], combo_col[1]) ] = df[combo_col[0]] * df[combo_col[1]] + if '-' in operations: + fe_df['{}_minus_{}'.format(combo_col[0], combo_col[1]) ] = df[combo_col[0]] - df[combo_col[1]] + if '+' in operations: + fe_df['{}_plus_{}'.format(combo_col[0], combo_col[1]) ] = df[combo_col[0]] + df[combo_col[1]] + + for each_col in copy_columns: + fe_df['{}_squared'.format(each_col) ] = df[each_col].pow(2) + return (fe_df) + + + def gen_groupby_cat_encode_features(self, data, cat_columns, num_column, + cat_encoder_name='JamesSteinEncoder'): + """ + Description of group_encoder + + Args: + data (pd.DataFrame): dataset + cat_columns (list): cat columns names + num_column (str): num column name + + Returns: + pd.DataFrame + + """ + + if isinstance(cat_encoder_name, str): + if cat_encoder_name in self.cat_encoder_names_list: + encoder = JamesSteinEncoder(cols=self.cat_features, model='beta', return_df = True, drop_invariant=True) + encoder.fit(X=data[cat_columns], y=data[num_column].values) + else: + print(f"{cat_encoder_name} is not supported!") + return ('', '') + else: + encoder = copy.deepcopy(cat_encoder_name) + + data_encodet = encoder.transform(X=data[cat_columns], y=data[num_column].values) + data_encodet = data_encodet.add_prefix('GroupEncoded_' + num_column + '_') + + return (data_encodet, encoder) + + def preproc_data(self, X_train=None, + X_test=None, + y_train=None, + cat_features=None, + cat_encoder_names=None, + clean_nan=True, + num_generator_features=True, + group_generator_features=True, + target_enc_cat_features=True, + normalization=True, + verbose=1,): + """ + Description of preproc_data: + dataset preprocessing function + + Args: + X_train=None (pd.DataFrame): + X_test=None (pd.DataFrame): + y_train=None (pd.DataFrame): + cat_features=None (list): + cat_encoder_names=None (list): + clean_nan=True (Bool): + num_generator_features=True (Bool): + group_generator_features=True (Bool): + + Returns: + X_train (pd.DataFrame) + X_test (pd.DataFrame) + + """ + + #### Sometimes there are duplicates in column names. You must remove them here. ### + cat_features = find_remove_duplicates(cat_features) + + # concat datasets for correct processing. + df_train = X_train.copy() + + if X_test is None: + data = df_train + test_data = None ### Set test_data to None if X_test is None + else: + test_data = X_test.copy() + test_data = remove_duplicate_cols_in_dataset(test_data) + data = copy.deepcopy(df_train) + + data = remove_duplicate_cols_in_dataset(data) + + # object & num features + object_features = list(data.columns[(data.dtypes == 'object') | (data.dtypes == 'category')]) + num_features = list(set(data.columns) - set(cat_features) - set(object_features) - {'test'}) + encodet_features_names = list(set(object_features + list(cat_features))) + + original_number_features = len(encodet_features_names) + count_number_features = df_train.shape[1] + + self.encodet_features_names = encodet_features_names + self.num_features_names = num_features + self.binary_features_names = [] + + # LabelEncode all Binary Features - leave the rest alone + cols = data.columns.tolist() + #### This sometimes errors because there are duplicate columns in a dataset ### + print('LabelEncode all Boolean Features. Leave the rest alone') + for feature in cols: + if data[feature].dtype == bool : + print(' boolean feature = ',feature) + + for feature in cols: + if (data[feature].dtype == bool): + data[feature] = data[feature].astype('category').cat.codes + if test_data is not None: + test_data[feature] = test_data[feature].astype('category').cat.codes + self.binary_features_names.append(feature) + + # Convert all Category features "Category" type variables if no encoding is specified + cat_only_encoders = [x for x in self.cat_encoder_names if x in self.cat_encoders_names] + if len(cat_only_encoders) > 0: + ### Just skip if this encoder is not in the list of category encoders ## + if encodet_features_names: + if cat_encoder_names is None: + for feature in encodet_features_names: + data[feature] = data[feature].fillna('missing') + data[feature] = data[feature].astype('category').cat.codes + if test_data is not None: + test_data[feature] = test_data[feature].fillna('missing') + test_data[feature] = test_data[feature].astype('category').cat.codes + else: + #### If an encoder is specified, then use that encoder to transform categorical variables + if verbose > 0: + print('> Generate Categorical Encoded features') + + copy_cat_encoder_names = copy.deepcopy(cat_encoder_names) + for encoder_name in copy_cat_encoder_names: + if verbose > 0: + print(' + To know more, click: %s' %self.cat_encoders_names[encoder_name][1]) + data_encodet, train_encoder = self.gen_cat_encodet_features(data[encodet_features_names], + encoder_name) + if not isinstance(data_encodet, str): + data = pd.concat([data, data_encodet], axis=1) + if test_data is not None: + test_encodet, _ = self.gen_cat_encodet_features(test_data[encodet_features_names], + train_encoder) + if not isinstance(test_encodet, str): + test_data = pd.concat([test_data, test_encodet], axis=1) + + if verbose > 0: + if not isinstance(data_encodet, str): + addl_features = data_encodet.shape[1] - original_number_features + count_number_features += addl_features + print(' + added ', addl_features, ' additional Features using',encoder_name) + + # Generate Target related Encoder features for cat variables: + + + target_encoders = [x for x in self.cat_encoder_names if x in self.target_encoders_names_list] + if len(target_encoders) > 0: + target_enc_cat_features = True + if target_enc_cat_features: + if encodet_features_names: + if verbose > 0: + print('> Generate Target Encoded categorical features') + + if len(target_encoders) == 0: + target_encoders = ['TargetEncoder'] ### set the default as TargetEncoder if nothing is specified + copy_target_encoders = copy.deepcopy(target_encoders) + for encoder_name in copy_target_encoders: + if verbose > 0: + print(' + To know more, click: %s' %self.target_encoders_names[encoder_name][1]) + data_encodet, train_encoder = self.gen_target_encodet_features(data[encodet_features_names], + self.y_train_source, encoder_name) + if not isinstance(data_encodet, str): + data = pd.concat([data, data_encodet], axis=1) + + if test_data is not None: + test_encodet, _ = self.gen_target_encodet_features(test_data[encodet_features_names],'', + train_encoder) + if not isinstance(test_encodet, str): + test_data = pd.concat([test_data, test_encodet], axis=1) + + + if verbose > 0: + if not isinstance(data_encodet, str): + addl_features = data_encodet.shape[1] - original_number_features + count_number_features += addl_features + print(' + added ', len(encodet_features_names) , ' additional Features using ', encoder_name) + + # Clean NaNs in Numeric variables only + if clean_nan: + if verbose > 0: + print('> Cleaned NaNs in numeric features') + data = self.clean_nans(data, cols=num_features) + if test_data is not None: + test_data = self.clean_nans(test_data, cols=num_features) + ### Sometimes, train has nulls while test doesn't and vice versa + if test_data is not None: + rem_cols = left_subtract(list(data),list(test_data)) + if len(rem_cols) > 0: + for rem_col in rem_cols: + test_data[rem_col] = 0 + elif len(left_subtract(list(test_data),list(data))) > 0: + rem_cols = left_subtract(list(test_data),list(data)) + for rem_col in rem_cols: + data[rem_col] = 0 + else: + print(' + test and train have similar NaN columns') + + # Generate interaction features for Numeric variables + if num_generator_features: + if len(num_features) > 1: + if verbose > 0: + print('> Generate Interactions features among Numeric variables') + fe_df = self.gen_numeric_interaction_features(data[num_features], + num_features, + operations=['/','*','-','+'],) + + if not isinstance(fe_df, str): + data = pd.concat([data,fe_df],axis=1) + if test_data is not None: + fe_test = self.gen_numeric_interaction_features(test_data[num_features], + num_features, + operations=['/','*','-','+'],) + if not isinstance(fe_test, str): + test_data = pd.concat([test_data, fe_test], axis=1) + + if verbose > 0: + if not isinstance(fe_df, str): + addl_features = fe_df.shape[1] + count_number_features += addl_features + print(' + added ', addl_features, ' Interaction Features ',) + + # Generate Group Encoded Features for Numeric variables only using all Categorical variables + if group_generator_features: + if encodet_features_names and num_features: + if verbose > 0: + print('> Generate Group-by Encoded Features') + print(' + To know more, click: %s' %self.target_encoders_names['JamesSteinEncoder'][1]) + + for num_col in num_features: + data_encodet, train_group_encoder = self.gen_groupby_cat_encode_features( + data, + encodet_features_names, + num_col,) + if not isinstance(data_encodet, str): + data = pd.concat([data, data_encodet],axis=1) + if test_data is not None: + test_encodet, _ = self.gen_groupby_cat_encode_features( + data, + encodet_features_names, + num_col,train_group_encoder) + if not isinstance(test_encodet, str): + test_data = pd.concat([test_data, test_encodet], axis=1) + + if verbose > 0: + addl_features = data_encodet.shape[1]*len(num_features) + count_number_features += addl_features + print(' + added ', addl_features, ' Group-by Encoded Features using JamesSteinEncoder') + + + # Drop source cat features + if not len(cat_encoder_names) == 0: + ### if there is no categorical encoding, then let the categorical_vars pass through. + ### If they have been transformed into Cat Encoded variables, then you can drop them! + data.drop(columns=encodet_features_names, inplace=True) + # In this case, there may be some inf values, replace them ###### + data.replace([np.inf, -np.inf], np.nan, inplace=True) + #data.fillna(0, inplace=True) + if test_data is not None: + if not len(cat_encoder_names) == 0: + ### if there is no categorical encoding, then let the categorical_vars pass through. + test_data.drop(columns=encodet_features_names, inplace=True) + test_data.replace([np.inf, -np.inf], np.nan, inplace=True) + #test_data.fillna(0, inplace=True) + + X_train = copy.deepcopy(data) + X_test = copy.deepcopy(test_data) + + # Normalization Data + if normalization: + if verbose > 0: + print('> Normalization Features') + columns_name = X_train.columns.values + scaler = StandardScaler().fit(X_train) + X_train = scaler.transform(X_train) + X_test = scaler.transform(X_test) + X_train = pd.DataFrame(X_train, columns=columns_name) + X_test = pd.DataFrame(X_test, columns=columns_name) + + if verbose > 0: + print('#'*50) + print('> Final Number of Features: ', (X_train.shape[1])) + print('#'*50) + print('New X_train rows: %s, X_test rows: %s' %(X_train.shape[0], X_test.shape[0])) + print('New X_train columns: %s, X_test columns: %s' %(X_train.shape[1], X_test.shape[1])) + if len(left_subtract(X_test.columns, X_train.columns)) > 0: + print("""There are more columns in test than train + due to missing columns being more in test than train. Continuing...""") + + return X_train, X_test +################################################################################ +def find_rare_class(series, verbose=0): + ######### Print the % count of each class in a Target variable ##### + """ + Works on Multi Class too. Prints class percentages count of target variable. + It returns the name of the Rare class (the one with the minimum class member count). + This can also be helpful in using it as pos_label in Binary and Multi Class problems. + """ + return series.value_counts().index[-1] +################################################################################# +def left_subtract(l1,l2): + lst = [] + for i in l1: + if i not in l2: + lst.append(i) + return lst +################################################################################# +def remove_duplicate_cols_in_dataset(df): + df = copy.deepcopy(df) + cols = df.columns.tolist() + number_duplicates = df.columns.duplicated().astype(int).sum() + if number_duplicates > 0: + print('Detected %d duplicate columns in dataset. Removing duplicates...' %number_duplicates) + df = df.loc[:,~df.columns.duplicated()] + return df +########################################################################### +# Removes duplicates from a list to return unique values - USED ONLYONCE +def find_remove_duplicates(values): + output = [] + seen = set() + for value in values: + if value not in seen: + output.append(value) + seen.add(value) + return output +################################################################################# diff --git a/build/lib/featurewiz/encoders.py b/build/lib/featurewiz/encoders.py new file mode 100644 index 0000000..be63276 --- /dev/null +++ b/build/lib/featurewiz/encoders.py @@ -0,0 +1,125 @@ +############################################################################### +# MIT License +# +# Copyright (c) 2020 Alex Lekov +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +############################################################################### +##### This amazing Library was created by Alex Lekov: Many Thanks to Alex! ### +##### https://github.com/Alex-Lekov/AutoML_Alex ### +############################################################################### +import pandas as pd +import numpy as np + +################################################################ + # Simple Encoders + # (do not use information about target) +################################################################ + +class FrequencyEncoder(): + """ + FrequencyEncoder + Conversion of category into frequencies. + Parameters + ---------- + cols : list of categorical features. + drop_invariant : not used + """ + def __init__(self, cols=None, drop_invariant=None): + """ + Description of __init__ + + Args: + cols=None (undefined): columns in dataset + drop_invariant=None (undefined): not used + + """ + self.cols = cols + self.counts_dict = None + + def fit(self, X: pd.DataFrame, y=None) -> pd.DataFrame: + """ + Description of fit + + Args: + X (pd.DataFrame): dataset + y=None (not used): not used + + Returns: + pd.DataFrame + + """ + counts_dict = {} + if self.cols is None: + self.cols = X.columns + for col in self.cols: + values = X[col].value_counts(dropna=False).index + n_obs = np.float(len(X)) + counts = list(X[col].value_counts(dropna=False) / n_obs) + counts_dict[col] = dict(zip(values, counts)) + self.counts_dict = counts_dict + + def transform(self, X: pd.DataFrame) -> pd.DataFrame: + """ + Description of transform + + Args: + X (pd.DataFrame): dataset + + Returns: + pd.DataFrame + + """ + counts_dict_test = {} + res = [] + for col in self.cols: + values = X[col].value_counts(1,dropna=False).index.tolist() + counts = X[col].value_counts(1,dropna=False).values.tolist() + counts_dict_test[col] = dict(zip(values, counts)) + + # if value is in "train" keys - replace "test" counts with "train" counts + for k in [ + key + for key in counts_dict_test[col].keys() + if key in self.counts_dict[col].keys() + ]: + counts_dict_test[col][k] = self.counts_dict[col][k] + res.append(X[col].map(counts_dict_test[col]).values.reshape(-1, 1)) + try: + res = np.hstack(res) + except: + pdb.set_trace() + X[self.cols] = res + return X + + def fit_transform(self, X: pd.DataFrame, y=None) -> pd.DataFrame: + """ + Description of fit_transform + + Args: + X (pd.DataFrame): dataset + y=None (undefined): not used + + Returns: + pd.DataFrame + + """ + self.fit(X, y) + X = self.transform(X) + return X diff --git a/build/lib/featurewiz/featurewiz.py b/build/lib/featurewiz/featurewiz.py new file mode 100644 index 0000000..3e9260d --- /dev/null +++ b/build/lib/featurewiz/featurewiz.py @@ -0,0 +1,3878 @@ +############################################################################## +#Copyright 2019 Google LLC +# +#Licensed under the Apache License, Version 2.0 (the "License"); +#you may not use this file except in compliance with the License. +#You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +#Unless required by applicable law or agreed to in writing, software +#distributed under the License is distributed on an "AS IS" BASIS, +#WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +#See the License for the specific language governing permissions and +#limitations under the License. +################################################################################# +##### This project is not an official Google project. It is not supported by #### +##### Google and Google specifically disclaims all warranties as to its quality,# +##### merchantability, or fitness for a particular purpose. #################### +################################################################################# +import numpy as np +np.random.seed(99) +import random +random.seed(42) +import pandas as pd +from sklearn.model_selection import KFold +from sklearn.model_selection import GridSearchCV +from sklearn.multioutput import MultiOutputClassifier, MultiOutputRegressor +from sklearn.multiclass import OneVsRestClassifier +import xgboost as xgb +from xgboost.sklearn import XGBClassifier +from xgboost.sklearn import XGBRegressor +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import StandardScaler +from sklearn.multiclass import OneVsRestClassifier +########### This is from category_encoders Library ################################################ +from category_encoders import HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder +from category_encoders import OneHotEncoder, HelmertEncoder, OrdinalEncoder, CountEncoder, BaseNEncoder +from category_encoders import TargetEncoder, CatBoostEncoder, WOEEncoder, JamesSteinEncoder +from category_encoders.glmm import GLMMEncoder +from sklearn.preprocessing import LabelEncoder +from category_encoders.wrapper import PolynomialWrapper +from .encoders import FrequencyEncoder +from .sulov_method import FE_remove_variables_using_SULOV_method +from .classify_method import classify_columns, EDA_find_remove_columns_with_infinity +from .ml_models import analyze_problem_type, get_sample_weight_array, check_if_GPU_exists +from .my_encoders import Groupby_Aggregator, My_LabelEncoder_Pipe, My_LabelEncoder +from .my_encoders import Rare_Class_Combiner, Rare_Class_Combiner_Pipe, FE_create_time_series_features +from .my_encoders import Column_Names_Transformer +from .stacking_models import DenoisingAutoEncoder, VariationalAutoEncoder, GANAugmenter, GAN + +from . import settings +settings.init() +################################################################################ +#### The warnings from Sklearn are so annoying that I have to shut it off ####### +import warnings +warnings.filterwarnings("ignore") +from sklearn.exceptions import DataConversionWarning +warnings.filterwarnings(action='ignore', category=DataConversionWarning) +with warnings.catch_warnings(): + warnings.simplefilter("ignore") +################################################################################ +def warn(*args, **kwargs): + pass +warnings.warn = warn +import logging +#################################################################################### +import re +import pdb +import pprint +from itertools import cycle, combinations +from collections import defaultdict, OrderedDict +import time +import sys +import xlrd +import statsmodels +from io import BytesIO +import base64 +from functools import reduce +import copy +import dask +import dask.dataframe as dd +#import dask_xgboost +import xgboost +from dask.distributed import Client, progress +import psutil +import json +from sklearn.model_selection import train_test_split +from .my_encoders import FE_convert_all_object_columns_to_numeric +####################################################################################################### +def classify_features(dfte, depVar, verbose=0): + dfte = copy.deepcopy(dfte) + if isinstance(depVar, list): + orig_preds = [x for x in list(dfte) if x not in depVar] + else: + orig_preds = [x for x in list(dfte) if x not in [depVar]] + ################# CLASSIFY COLUMNS HERE ###################### + var_df = classify_columns(dfte[orig_preds], verbose) + ##### Classify Columns ################ + IDcols = var_df['id_vars'] + discrete_string_vars = var_df['nlp_vars']+var_df['discrete_string_vars'] + cols_delete = var_df['cols_delete'] + bool_vars = var_df['string_bool_vars'] + var_df['num_bool_vars'] + int_vars = var_df['int_vars'] + categorical_vars = var_df['cat_vars'] + var_df['factor_vars'] + int_vars + bool_vars + date_vars = var_df['date_vars'] + if len(var_df['continuous_vars'])==0 and len(int_vars)>0: + continuous_vars = var_df['int_vars'] + categorical_vars = left_subtract(categorical_vars, int_vars) + int_vars = [] + else: + continuous_vars = var_df['continuous_vars'] + preds = [x for x in orig_preds if x not in IDcols+cols_delete+discrete_string_vars] + if len(IDcols+cols_delete+discrete_string_vars) == 0: + print(' No variables were removed since no ID or low-information variables found in data set') + else: + print(' %d variable(s) to be removed since ID or low-information variables' + %len(IDcols+cols_delete+discrete_string_vars)) + if len(IDcols+cols_delete+discrete_string_vars) <= 30: + print(' \tvariables removed = %s' %(IDcols+cols_delete+discrete_string_vars)) + else: + print(' \tmore than %s variables to be removed; too many to print...' %len(IDcols+cols_delete+discrete_string_vars)) + ############# Check if there are too many columns to visualize ################ + ppt = pprint.PrettyPrinter(indent=4) + if verbose >= 2 and len(cols_list) <= max_cols_analyzed: + marthas_columns(dft,verbose) + if verbose==1 and len(cols_list) <= max_cols_analyzed: + print(" Columns to delete:") + ppt.pprint(' %s' % cols_delete) + print(" Boolean variables %s ") + ppt.pprint(' %s' % bool_vars) + print(" Categorical variables %s ") + ppt.pprint(' %s' % categorical_vars) + print(" Continuous variables %s " ) + ppt.pprint(' %s' % continuous_vars) + print(" Discrete string variables %s " ) + ppt.pprint(' %s' % discrete_string_vars) + print(" Date and time variables %s " ) + ppt.pprint(' %s' % date_vars) + print(" ID variables %s ") + ppt.pprint(' %s' % IDcols) + print(" Target variable %s ") + ppt.pprint(' %s' % depVar) + elif verbose==1 and len(cols_list) > max_cols_analyzed: + print(' Total columns > %d, too numerous to list.' %max_cols_analyzed) + features_dict = dict([('IDcols',IDcols),('cols_delete',cols_delete),('bool_vars',bool_vars),( + 'categorical_vars',categorical_vars), + ('continuous_vars',continuous_vars),('discrete_string_vars',discrete_string_vars), + ('date_vars',date_vars)]) + return features_dict +####################################################################################################### +def marthas_columns(data,verbose=0): + """ + This program is named in honor of my one of students who came up with the idea for it. + It's a neat way of printing data types and information compared to the boring describe() function in Pandas. + """ + data = data[:] + print('Data Set Shape: %d rows, %d cols' % data.shape) + if data.shape[1] > 30: + print('Too many columns to print') + else: + if verbose==1: + print(' Additional details on columns:') + for col in data.columns: + print('\t* %s:\t%d missing, %d uniques, most common: %s' % ( + col, + data[col].isnull().sum(), + data[col].nunique(), + data[col].value_counts().head(2).to_dict() + )) + print('--------------------------------------------------------------------') +################################################################################ +######### NEW And FAST WAY to CLASSIFY COLUMNS IN A DATA SET ####### +################################################################################ +################################################################################# +def lenopenreadlines(filename): + with open(filename) as f: + return len(f.readlines()) +######################################################################################### +from collections import Counter +import time +from sklearn.feature_selection import chi2, mutual_info_regression, mutual_info_classif +from sklearn.feature_selection import SelectKBest +################################################################################## +def load_file_dataframe(dataname, sep=",", header=0, verbose=0, + nrows=None, parse_dates=False, target='', is_test_flag=False): + start_time = time.time() + ### This is where you have to make sure target is not empty ##### + if not isinstance(dataname,str): + dfte = copy.deepcopy(dataname) + if isinstance(target, str): + if not is_test_flag: + if len(target) == 0: + modelt = 'Clustering' + print('featurewiz does not work on clustering or unsupervised problems. Returning...') + return dataname + else: + modelt, _ = analyze_problem_type(dataname[target], target) + else: + ### For test data, just check the target value which will be given as odeltype ## + modelt = copy.deepcopy(target) + else: + ### Target is a list or None ############ + if not is_test_flag: + if target is None or len(target) == 0: + modelt = 'Clustering' + print('featurewiz does not work on clustering or unsupervised problems. Returning...') + return dataname + else: + modelt, _ = analyze_problem_type(dataname[target], target) + else: + ## For test data, the modeltype is given in the target variable + modelt = copy.deepcopy(target) + ########################### This is where we load file or data frame ############### + elif isinstance(dataname,str): + if dataname == '': + print(' No file given. Continuing...') + return None + #### this means they have given file name as a string to load the file ##### + codex = ['ascii', 'utf-8', 'iso-8859-1', 'cp1252', 'latin1'] + ## this is the total number of rows in df ### + ############################################################################### + if dataname != '' and dataname.endswith(('csv')): + try: + ### You can read the entire data into pandas first and then stratify split it ## + ### If you don't stratify it, then you will have less classes than 2 error ### + dfte = pd.read_csv(dataname, sep=sep, header=header, encoding=None, + parse_dates=parse_dates) + print(' pandas default encoder does not work for this file. Trying other encoders...') + except: + for code in codex: + try: + dfte = pd.read_csv(dataname, sep=sep, header=header, + encoding=code, parse_dates=parse_dates) + break + except: + continue + ######### If the file is not loadable, then give an error message ######### + try: + print(' Shape of your Data Set loaded: %s' %(dfte.shape,)) + except: + print(' File not loadable. Please check your file path or encoding format and try again.') + return dataname + elif dataname.endswith(('xlsx','xls','txt')): + #### It's very important to get header rows in Excel since people put headers anywhere in Excel# + dfte = pd.read_excel(dataname,header=header, parse_dates=parse_dates) + elif dataname.endswith(('gzip', 'bz2', 'zip', 'xz')): + print(' Reading compressed file...') + try: + #### Dont use skip_function in zip files ##### + compression = 'infer' + dfte = pd.read_csv(dataname, sep=sep, header=header, encoding=None, + compression=compression, parse_dates=parse_dates) + except: + print(' Could not read compressed file. Please unzip and try again...') + return dataname + elif isinstance(dataname, pd.DataFrame): + dfte = copy.deepcopy(dataname) + else: + print('Dataname input must be a filename with path to that file or a Dataframe') + return None + ######################### This is where you sample rows ############################ + #### this means they now have a dataframe and you must sample it correctly ##### + #### Now that you have read the file, you must sample it ############ + #################################################################################### + if not nrows is None: + if nrows < dfte.shape[0]: + if modelt == 'Regression': + dfte = dfte[:nrows] + print(' sequentially select %s max_rows from dataset %d...' %(nrows, dfte.shape[0])) + else: + test_size = 1 - (nrows/dfte.shape[0]) + print(' stratified split %d rows from given %s' %(nrows, dfte.shape[0])) + dfte, _ = train_test_split(dfte, test_size=test_size, stratify=dfte[target], + shuffle=True, random_state=99) + if len(np.array(list(dfte))[dfte.columns.duplicated()]) > 0: + print('You have duplicate column names in your data set. Removing duplicate columns now...') + dfte = dfte[list(dfte.columns[~dfte.columns.duplicated(keep='first')])] + return dfte +########################################################################################## +##### This function loads a time series data and sets the index as a time series +def load_dask_data(filename, sep, ): + """ + This function loads a given filename into a dask dataframe. + If the input is a pandas DataFrame, it converts it into a dask dataframe. + Note that filename should contain the full path to the file. + """ + n_workers = get_cpu_worker_count() + if isinstance(filename, str): + dft = dd.read_csv(filename, blocksize='default') + print(' Too big to fit into pandas. Hence loaded file %s into a Dask dataframe ...' % filename) + else: + ### If filename is not a string, it must be a dataframe and can be loaded + dft = dd.from_pandas(filename, npartitions=n_workers) + return dft +################################################################################## +# Removes duplicates from a list to return unique values - USED ONLYONCE +def find_remove_duplicates(values): + output = [] + seen = set() + for value in values: + if value not in seen: + output.append(value) + seen.add(value) + return output +################################################################################# +import copy +def FE_drop_rows_with_infinity(df, cols_list, fill_value=None): + """ + This feature engineering function will fill infinite values in your data with a fill_value. + You might need this function during deep_learning models where infinite values don't work. + You can also leave the fill_value as None which means we will drop the rows with infinity. + This function checks for both negative and positive infinity values to fill or remove. + """ + # first you must drop rows that have inf in them #### + print(' Shape of dataset initial: %s' %(df.shape[0])) + corr_list_copy = copy.deepcopy(cols_list) + init_rows = df.shape[0] + if fill_value: + for col in corr_list_copy: + ### Capping using the n largest value based on n given in input. + maxval = df[col].max() ## what is the maximum value in this column? + minval = df[col].min() + if maxval == np.inf: + sorted_list = sorted(df[col].unique()) + ### find the n_smallest values after the maximum value based on given input n + next_best_value_index = sorted_list.index(np.inf) - 1 + capped_value = sorted_list[next_best_value_index] + df.loc[df[col]==maxval, col] = capped_value ## maximum values are now capped + if minval == -np.inf: + sorted_list = sorted(df[col].unique()) + ### find the n_smallest values after the maximum value based on given input n + next_best_value_index = sorted_list.index(-np.inf)+1 + capped_value = sorted_list[next_best_value_index] + df.loc[df[col]==minval, col] = capped_value ## maximum values are now capped + print(' capped all rows with infinite values in data') + else: + for col in corr_list_copy: + df = df[df[col]!=np.inf] + df = df[df[col]!=-np.inf] + dropped_rows = init_rows - df.shape[0] + print(' dropped %d rows due to infinite values in data' %dropped_rows) + print(' Shape of dataset after dropping rows: %s' %(df.shape[0])) + ### Double check that all columns have been fixed ############### + cols_with_infinity = EDA_find_remove_columns_with_infinity(df) + if cols_with_infinity: + print(' There are still %d columns with infinite values. Returning...' %len(cols_with_infinity)) + else: + print(' There are no more columns with infinite values') + return df +################################################################################## +def count_freq_in_list(lst): + """ + This counts the frequency of items in a list but MAINTAINS the order of appearance of items. + This order is very important when you are doing certain functions. Hence this function! + """ + temp=np.unique(lst) + result = [] + for i in temp: + result.append((i,lst.count(i))) + return result +############################################################################################### +def left_subtract(l1,l2): + lst = [] + for i in l1: + if i not in l2: + lst.append(i) + return lst +################################################################################# +def return_factorized_dict(ls): + """ + ###### Factorize any list of values in a data frame using this neat function + if your data has any NaN's it automatically marks it as -1 and returns that for NaN's + Returns a dictionary mapping previous values with new values. + """ + factos = pd.unique(pd.factorize(ls)[0]) + categs = pd.unique(pd.factorize(ls)[1]) + if -1 in factos: + categs = np.insert(categs,np.where(factos==-1)[0][0],np.nan) + return dict(zip(categs,factos)) +################################################################################### +from sklearn.feature_selection import chi2, mutual_info_regression, mutual_info_classif +from sklearn.feature_selection import SelectKBest +from .databunch import DataBunch +from .encoders import FrequencyEncoder + +from sklearn.model_selection import train_test_split +def featurewiz(dataname, target, corr_limit=0.8, verbose=0, sep=",", header=0, + test_data='', feature_engg='', category_encoders='', dask_xgboost_flag=False, + nrows=None, skip_sulov=False, skip_xgboost=False, **kwargs): + """ + ################################################################################# + ############### F E A T U R E W I Z A R D ################## + ################ featurewiz library developed by Ram Seshadri ################# + # featurewiz utilizes SULOV METHOD which is a fast method for feature selection # + ##### SULOV also means Searching for Uncorrelated List Of Variables (:-) ###### + ############### A L L R I G H T S R E S E R V E D ################ + ################################################################################# + Featurewiz is the main module of this library. You will create features and select + the best features using the SULOV method and permutation based XGB feature importance. + It returns a list of important features from your dataframe after feature engineering. + Since we do label encoding, you can send both categorical and numeric vars. + You can also send in features with NaN's in them. + ################################################################################# + Inputs: + dataname: training data set you want to input. dataname could be a datapath+filename or a dataframe. + featurewiz will detect whether your input is a filename or a dataframe and load it automatically. + target: name of the target variable in the data set. Also known as dependent variable. + corr_limit: if you want to set your own threshold for removing variables as + highly correlated, then give it here. The default is 0.7 which means variables less + than -0.7 and greater than 0.7 in pearson's correlation will be candidates for removal. + verbose: This has 3 possible states: + 0 limited output. Great for running this silently and getting fast results. + 1 more verbiage. Great for knowing how results were and making changes to flags in input. + 2 SULOV charts and output. Great for finding out what happens under the hood for SULOV method. + test_data: If you want to transform test data in the same way you are transforming dataname, you can. + test_data could be the name of a datapath+filename or a dataframe. featurewiz will detect whether + your input is a filename or a dataframe and load it automatically. Default is empty string. + feature_engg: You can let featurewiz select its best encoders for your data set by setting this flag + for adding feature engineering. There are three choices. You can choose one, two or all three in a list. + 'interactions': This will add interaction features to your data such as x1*x2, x2*x3, x1**2, x2**2, etc. + 'groupby': This will generate Group By features to your numeric vars by grouping all categorical vars. + 'target': This will encode & transform all your categorical features using certain target encoders. + Default is empty string (which means no additional feature engineering to be performed) + category_encoders: Instead of above method, you can choose your own kind of category encoders from below. + Recommend you do not use more than two of these. + Featurewiz will automatically select only two from your list. + Default is empty string (which means no encoding of your categorical features) + ['HashingEncoder', 'SumEncoder', 'PolynomialEncoder', 'BackwardDifferenceEncoder', + 'OneHotEncoder', 'HelmertEncoder', 'OrdinalEncoder', 'FrequencyEncoder', 'BaseNEncoder', + 'TargetEncoder', 'CatBoostEncoder', 'WOEEncoder', 'JamesSteinEncoder'] + dask_xgboost_flag: default = False. This flag enables DASK by default so that you can process large + data sets faster using parallel processing. It detects the number of CPUs and GPU's in your machine + automatically and sets the num of workers for DASK. It also uses DASK XGBoost to run it. + nrows: default = None: None means all rows will be utilized. If you want to sample "N" rows, set nrows=N. + skip_sulov: a new flag to skip SULOV method. It will automatically go straight to recursive xgboost. + skip_xgboost: a new flag to skip recursive xgboost. + ######## Featurewiz Output ############################# + Output: Tuple + Featurewiz can output either a list of features or one dataframe or two depending on what you send in. + 1. features: featurewiz will return just a list of important features + in your data if you send in just a dataset. + 2. trainm: modified train dataframe is the dataframe that is modified + with engineered and selected features from dataname. + 3. testm: modified test dataframe is the dataframe that is modified with + engineered and selected features from test_data + """ + if verbose: + print('############################################################################################') + print('############ F A S T F E A T U R E E N G G A N D S E L E C T I O N ! ########') + print("# Be judicious with featurewiz. Don't use it to create too many un-interpretable features! #") + print('############################################################################################') + print('featurewiz has selected %s as the correlation limit. Change this limit to fit your needs...' %corr_limit) + if not nrows is None: + print('ALERT: nrows=%s. Hence featurewiz will randomly sample that many rows.' %nrows) + print(' Change nrows=None if you want all rows...') + ### set all the defaults here ############################################## + dataname = copy.deepcopy(dataname) + max_nums = 30 + max_cats = 15 + maxrows = 10000 + RANDOM_SEED = 42 + mem_limit = 500 ### amount of memory consumed by pandas df before reducing_mem function called + ############################################################################ + cat_encoders_list = list(settings.cat_encoders_names.keys()) + ### Just set defaults here which can be overridden by user input #### + cat_vars = [] + if kwargs: + for key, value in zip(kwargs.keys(), kwargs.values()): + print('You supplied %s = %s' %(key, value)) + ###### Now test the next set of kwargs ### + if key == 'cat_vars': + if isinstance(value, list): + cat_vars = value + elif isinstance(value, str): + cat_vars = value + else: + print('cat vars must be a list or a string') + return + ###################################################################################### + ##### MAKING FEATURE_TYPE AND FEATURE_GEN SELECTIONS HERE ############# + ###################################################################################### + feature_generators = ['interactions', 'groupby', 'target'] + feature_gen = '' + if feature_engg: + if isinstance(feature_engg, str): + if feature_engg in feature_generators: + feature_gen = [feature_engg] + else: + print('feature engg types must be one of three strings: %s' %feature_generators) + return + elif isinstance(feature_engg, list): + feature_gen = copy.deepcopy(feature_engg) + else: + print(' Skipping feature engineering since no feature_engg input...') + ####### Now start doing the pre-processing ################### + feature_type = '' + if category_encoders: + if isinstance(category_encoders, str): + feature_type = [category_encoders] + elif isinstance(category_encoders, list): + feature_type = category_encoders[:2] ### Only two will be allowed at a time + else: + print('Skipping category encoding since no category encoders specified in input...') + ###################################################################################### + ################## L O A D T R A I N D A T A ############################## + ########## dataname will be the name of the pandas version of train data ###### + ########## train will be the Dask version of train data ###### + ###################################################################################### + if isinstance(dataname, str): + #### This is where we get a filename as a string as an input ################# + if re.search(r'(.ftr)', dataname) or re.search(r'(.feather)', dataname): + print("""**INFO: Feather format allowed. Loading feather formatted file...**""") + import feather + dataname = pd.read_feather(dataname, use_threads=True) + train = load_dask_data(dataname, sep) + else: + if verbose: + print("""**INFO: to increase file loading performance, convert huge `csv` files to `feather` format""") + print("""**INFO: Use `df.reset_index(drop=True).to_feather("path/to/save/file.ftr")` to save file in feather format**""") + if dask_xgboost_flag: + try: + print(' Since dask_xgboost_flag is True, reducing memory size and loading into dask') + dataname = pd.read_csv(dataname, sep=sep, header=header, nrows=nrows) + if (dataname.memory_usage().sum()/1000000) > mem_limit: + dataname = reduce_mem_usage(dataname) + train = load_dask_data(dataname, sep) + except: + print('File could not be loaded into dask. Check the path or filename and try again') + return None + else: + #### There is no dask flag so load it into a regular pandas dataframe #### + train = load_file_dataframe(dataname, sep=sep, header=header, verbose=verbose, + nrows=nrows, target=target) + if (train.memory_usage().sum()/1000000) > mem_limit: + dataname = reduce_mem_usage(train) + else: + dataname = copy.deepcopy(train) + else: + #### This is where we get a dataframe as an input ################# + if dask_xgboost_flag: + if not nrows is None: + dataname = dataname.sample(n=nrows, replace=True, random_state=9999) + print('Sampling %s rows from dataframe given' %nrows) + print(' Since dask_xgboost_flag is True, reducing memory size and loading into dask') + if (dataname.memory_usage().sum()/1000000) > mem_limit: + dataname = reduce_mem_usage(dataname) + train = load_dask_data(dataname, sep) + else: + train = load_file_dataframe(dataname, sep=sep, header=header, verbose=verbose, + nrows=nrows, target=target) + if (train.memory_usage().sum()/1000000) > mem_limit: + dataname = reduce_mem_usage(train) + else: + dataname = copy.deepcopy(train) + print(' Loaded train data. Shape = %s' %(dataname.shape,)) + ################## L O A D T E S T D A T A ###################### + dataname = remove_duplicate_cols_in_dataset(dataname) + + #### Convert mixed data types to string data type ############################ + #dataname = FE_convert_mixed_datatypes_to_string(dataname) + + ###### XGBoost cannot handle special chars in column names ########### + uniq = Column_Names_Transformer() + dataname = uniq.fit_transform(dataname) + new_col_names = uniq.new_column_names + old_col_names = uniq.old_column_names + special_char_flag = uniq.transformed_flag + + ### Suppose you have changed the names, thenn you must load it in dask again ## + if special_char_flag: + if dask_xgboost_flag: + train = load_dask_data(dataname, sep) + + ###### Now save the old and new columns in a dictionary to use them later ### + col_name_mapper = dict(zip(new_col_names, old_col_names)) + col_name_replacer = {y: x for (x, y) in col_name_mapper.items()} + item_replacer = col_name_replacer.get # For faster gets. + + #### You need to change the target name if you have changed the column names ### + if special_char_flag: + if isinstance(target, str): + targets = [target] + targets = [item_replacer(n, n) for n in targets] + target = targets[0] + else: + targets = copy.deepcopy(target) + target = [item_replacer(n, n) for n in targets] + + train_index = dataname.index + + if isinstance(target, str): + if len(target) == 0: + cols_list = list(dataname) + settings.modeltype = 'Clustering' + print('featurewiz does not work on clustering or unsupervised problems. Returning...') + return old_col_names, dataname + else: + settings.modeltype, _ = analyze_problem_type(dataname[target], target) + cols_list = left_subtract(list(dataname),target) + else: + ### Target is a list or None ############ + if target is None or len(target) == 0: + cols_list = list(dataname) + settings.modeltype = 'Clustering' + print('featurewiz does not work on clustering or unsupervised problems. Returning...') + return old_col_names, dataname + else: + settings.modeltype, _ = analyze_problem_type(dataname[target], target) + cols_list = left_subtract(list(dataname),target) + + ###################################################################################### + ################## L O A D T E S T D A T A ############################# + ########## test_data will be the name of the pandas version of test data ##### + ########## test will be the name of the dask dataframe version of test data ##### + ###################################################################################### + if isinstance(test_data, str): + if test_data != '': + if re.search(r'(.ftr)', test_data): + print("""**INFO: Feather format allowed. Loading feather file...**""") + import feather + test_data = pd.read_feather(test_data, use_threads=True) + test = load_dask_data(test_data, sep) + else: + if verbose: + print("""**INFO: to increase file loading performance, convert huge `csv` files to `feather` format using `df.to_feather("path/to/save/file.feather")`**""") + print('**INFO: featurewiz can now read feather formatted files...***') + ### only if test_data is a filename load this ##### + print('Loading test data filename = %s...' %test_data) + if dask_xgboost_flag: + print(' Since dask_xgboost_flag is True, reducing memory size and loading into dask') + ### nrows does not apply to test data in the case of featurewiz ############### + test_data = load_file_dataframe(test_data, sep=sep, header=header, verbose=verbose, + nrows=None, target=settings.modeltype, is_test_flag=True) + ### sometimes, test_data returns None if there is an error. ########## + if test_data is not None: + test_data = reduce_mem_usage(test_data) + ### test_data is the pandas dataframe object and test is dask dataframe object ## + test = load_dask_data(test_data, sep) + else: + #### load the entire test dataframe - there is no limit applicable there ######### + test_data = load_file_dataframe(test_data, sep=sep, header=header, + verbose=verbose, nrows=None, target=settings.modeltype, is_test_flag=True) + test = copy.deepcopy(test_data) + else: + print('No test data filename given...') + test_data = None + test = None + else: + print('loading the entire test dataframe - there is no nrows limit applicable #########') + test_data = load_file_dataframe(test_data, sep=sep, header=header, + verbose=verbose, nrows=None, target=settings.modeltype, is_test_flag=True) + test = copy.deepcopy(test_data) + ### sometimes, test_data returns None if there is an error. ########## + if test_data is not None: + test_data = remove_duplicate_cols_in_dataset(test_data) + test_index = test_data.index + print(' Loaded test data. Shape = %s' %(test_data.shape,)) + ####### Once again remove special chars in test data as well ### + test_data = uniq.transform(test_data) + + ### Suppose you have changed the names, thenn you must load it in dask again ## + if special_char_flag: + if dask_xgboost_flag: + ### Re-load test into dask in case names have been changed ### + test = load_dask_data(test_data, sep) + ##### convert mixed data types to string ############ + #test_data = FE_convert_mixed_datatypes_to_string(test_data) + #test = FE_convert_mixed_datatypes_to_string(test) + ############# C L A S S I F Y F E A T U R E S #################### + if nrows is None: + nrows_limit = maxrows + else: + nrows_limit = int(min(nrows, maxrows)) + #### you can use targets as a list wherever you choose ##### + if isinstance(target, str): + targets = [target] + else: + targets = copy.deepcopy(target) + if dataname.shape[0] >= nrows_limit: + print('Classifying features using a random sample of %s rows from dataset...' %nrows_limit) + ##### you can use nrows_limit to select a small sample from data set ######################## + train_small = EDA_randomly_select_rows_from_dataframe(dataname, targets, nrows_limit, DS_LEN=dataname.shape[0]) + features_dict = classify_features(train_small, target) + else: + features_dict = classify_features(dataname, target) + #### Now we have to drop certain cols that must be deleted ##################### + remove_cols = features_dict['discrete_string_vars'] + features_dict['cols_delete'] + if len(remove_cols) > 0: + print('train data shape before dropping %d columns = %s' %(len(remove_cols), dataname.shape,)) + dataname.drop(remove_cols, axis=1, inplace=True) + print('\ttrain data shape after dropping columns = %s' %(dataname.shape,)) + train = load_dask_data(dataname, sep) + if not test_data is None: + test_data.drop(remove_cols, axis=1, inplace=True) + test = load_dask_data(test_data, sep) + ################ Load data frame with date var features correctly this time ################ + if len(features_dict['date_vars']) > 0: + print('Caution: Since there are date-time variables in dataset, it is best to load them using pandas') + dask_xgboost_flag = False ### Set the dask flag to be False since it is now becoming Pandas dataframe + date_time_vars = features_dict['date_vars'] + dataname = load_file_dataframe(dataname, sep=sep, header=header, verbose=verbose, + nrows=nrows, parse_dates=date_time_vars, target=target) + if (dataname.memory_usage().sum()/1000000) > mem_limit: + dataname = reduce_mem_usage(dataname) + train = load_dask_data(dataname, sep) + if not test_data is None: + ### You must load the entire test data - there is no limit there ################## + ### test_data is the pandas dataframe object and test is dask dataframe object ## + test_data = load_file_dataframe(test_data, sep=sep, header=header, verbose=verbose, + nrows=nrows, parse_dates=date_time_vars, target=settings.modeltype, is_test_flag=True ) + test = copy.deepcopy(test_data) + else: + test_data = None + test = None + else: + train_index = dataname.index + if test_data is not None: + test_index = test_data.index + ################ X G B O O S T D E F A U L T S ###################################### + #### If there are more than 30 categorical variables in a data set, it is worth reducing features. + #### Otherwise. XGBoost is pretty good at finding the best features whether cat or numeric ! + ################################################################################################# + + start_time = time.time() + n_splits = 5 + max_depth = 8 + ###################### I M P O R T A N T D E F A U L T S ############## + subsample = 0.7 + col_sub_sample = 0.7 + test_size = 0.2 + #print('test_size = %s' %test_size) + seed = 1 + early_stopping = 5 + ####### All the default parameters are set up now ######### + kf = KFold(n_splits=n_splits) + ######### G P U P R O C E S S I N G B E G I N S ############ + ###### This is where we set the CPU and GPU parameters for XGBoost + GPU_exists = check_if_GPU_exists(verbose) + n_workers = get_cpu_worker_count() + ##### Set the Scoring Parameters here based on each model and preferences of user ### + cpu_params = {} + param = {} + cpu_tree_method = 'hist' + tree_method = 'hist' + n_estimators = 100 + cpu_params['nthread'] = -1 + cpu_params['tree_method'] = 'hist' + cpu_params['eta'] = 0.01 + cpu_params['subsample'] = 0.5 + cpu_params['grow_policy'] = 'depthwise' #'lossguide' + cpu_params['n_estimators'] = n_estimators + cpu_params['max_depth'] = max_depth + cpu_params['max_leaves'] = 0 + cpu_params['verbosity'] = 0 + cpu_params['gpu_id'] = 0 + cpu_params['updater'] = 'grow_colmaker' + cpu_params['predictor'] = 'cpu_predictor' + cpu_params['num_parallel_tree'] = 1 + if GPU_exists: + ### This has been fixed ### + tree_method = 'gpu_hist' + param['nthread'] = -1 + param['tree_method'] = 'gpu_hist' + param['eta'] = 0.01 + param['subsample'] = 0.5 + param['grow_policy'] = 'depthwise' # 'lossguide' # + param['n_estimators'] = n_estimators + param['max_depth'] = max_depth + param['max_leaves'] = 0 + param['verbosity'] = 0 + param['gpu_id'] = 0 + param['updater'] = 'grow_gpu_hist' #'prune' + param['predictor'] = 'gpu_predictor' + param['num_parallel_tree'] = 1 + gpuid = 0 + if verbose: + print(' Tuning XGBoost using GPU hyper-parameters. This will take time...') + else: + param = copy.deepcopy(cpu_params) + gpuid = None + if verbose: + print(' Tuning XGBoost using CPU hyper-parameters. This will take time...') + ################################################################################# + ############# D E T E C T SINGLE OR MULTI-LABEL PROBLEM ################# + ################################################################################# + if isinstance(target, str): + target = [target] + settings.multi_label = False + else: + if len(target) <= 1: + settings.multi_label = False + else: + settings.multi_label = True + #### You need to make sure only Single Label problems are handled in target encoding! + if settings.multi_label: + if verbose: + print('Turning off Target encoding for multi-label problems like this data set...') + print(' since Feature Engineering module cannot handle Multi Label Targets, turnoff target_enc_cat_features to False') + target_enc_cat_features = False + else: + ## If target is specified in feature_gen then use it to Generate target encoded features + target_enc_cat_features = 'target' in feature_gen + ###################################################################################### + ######## C L A S S I F Y V A R I A B L E S ########################## + ###### Now we detect the various types of variables to see how to convert them to numeric + ###################################################################################### + date_cols = features_dict['date_vars'] + if len(features_dict['date_vars']) > 0: + date_time_vars = copy.deepcopy(date_cols) + #### Do this only if date time columns exist in your data set! + date_col_mappers = {} + for date_col in date_cols: + print('Processing %s column for date time features....' %date_col) + dataname, ts_adds = FE_create_time_series_features(dataname, date_col) + date_col_mapper = dict([(x,date_col) for x in ts_adds]) + date_col_mappers.update(date_col_mapper) + #print(' Adding %d column(s) from date-time column %s in train' %(len(date_col_adds_train),date_col)) + #train = train.join(date_df_train, rsuffix='2') + if isinstance(test_data,str) or test_data is None: + ### do nothing #### + pass + else: + print(' Adding same time series features to test data...') + test_data, _ = FE_create_time_series_features(test_data, date_col, ts_adds) + #date_col_adds_test_data = left_subtract(date_df_test.columns.tolist(),date_col) + ### Now time to remove the date time column from all further processing ## + #test = test.join(date_df_test, rsuffix='2') + ### Now time to continue with our further processing ## + idcols = features_dict['IDcols'] + if isinstance(test_data,str) or test_data is None: + pass + else: + test_ids = test_data[idcols] + train_ids = dataname[idcols] ### this saves the ID columns of dataname + if cat_vars: + cols_in_both = [x for x in cat_vars if x in features_dict['cols_delete']] + cat_vars = left_subtract(cat_vars, features_dict['cols_delete']) + if len(cols_in_both) > 0: + print('Removing %s columns(s) which are in both cols to be deleted and cat vars given as input' %cols_in_both) + cols_to_remove = features_dict['cols_delete'] + idcols + features_dict['discrete_string_vars'] + print('Removing %d columns from further processing since ID or low information variables' %len(cols_to_remove)) + preds = [x for x in list(dataname) if x not in target+cols_to_remove] + ### This is where we sort the columns to make sure that the order of columns doesn't matter in selection ########### + #preds = np.sort(preds) + if verbose: + print(' After removing redundant variables from further processing, features left = %d' %len(preds)) + numvars = dataname[preds].select_dtypes(include = 'number').columns.tolist() + if len(numvars) > max_nums: + if feature_gen: + print('\nWarning: Too many extra features will be generated by featurewiz. This may take time...') + if cat_vars: + ### if input is given for cat_vars, use it! + catvars = copy.deepcopy(cat_vars) + numvars = left_subtract(preds, catvars) + else: + catvars = left_subtract(preds, numvars) + if len(catvars) > max_cats: + if feature_type: + print('\nWarning: Too many extra features will be generated by category encoding. This may take time...') + ###### C R E A T I N G I N T X N V A R S F R O M C A T V A R S ##################### + if np.where('interactions' in feature_gen,True, False).tolist(): + if len(catvars) > 1: + + num_combos = len(list(combinations(catvars,2))) + print('Adding %s interactions between %s categorical_vars %s...' %( + num_combos, len(catvars), catvars)) + dataname = FE_create_interaction_vars(dataname, catvars) + train = FE_create_interaction_vars(train, catvars) + catvars = left_subtract(dataname.columns.tolist(), numvars) + catvars = left_subtract(catvars, target) + preds = left_subtract(dataname.columns.tolist(), target) + if not test_data is None: + test_data = FE_create_interaction_vars(test_data, catvars) + test = FE_create_interaction_vars(test, catvars) + else: + if verbose: + print('No interactions created for categorical vars since number less than 2') + else: + if verbose: + print('No interactions created for categorical vars since feature engg does not specify it') + ##### Now we need to re-set the catvars again since we have created new features ##### + rem_vars = copy.deepcopy(catvars) + ########## Now we need to select the right model to run repeatedly #### + if settings.modeltype != 'Regression': + ########################################################################## + ########### L A B E L E N C O D I N G O F T A R G E T ######### + ########################################################################## + ### This is to convert the target labels to proper numeric columns ###### + target_conversion_flag = False + cat_targets = dataname[target].select_dtypes(include='object').columns.tolist() + dataname[target].select_dtypes(include='category').columns.tolist() + copy_targets = copy.deepcopy(targets) + for each_target in copy_targets: + if cat_targets or sorted(np.unique(dataname[each_target].values))[0] != 0: + print(' target labels need to be converted...') + target_conversion_flag = True + ### check if they are not starting from zero ################## + copy_targets = copy.deepcopy(target) + for each_target in copy_targets: + if target_conversion_flag: + mlb = My_LabelEncoder() + dataname[each_target] = mlb.fit_transform(dataname[each_target]) + try: + ## After converting train, just load it into dask again ## + train[each_target] = dd.from_pandas(dataname[each_target], npartitions=n_workers) + except: + print('Could not convert dask dataframe target into numeric. Check your input. Continuing...') + if test_data is not None: + if each_target in test_data.columns: + test_data[each_target] = mlb.transform(test_data[each_target]) + try: + ## After converting test, just load it into dask again ## + test[each_target] = dd.from_pandas(test_data[each_target], npartitions=n_workers) + except: + print('Could not convert dask dataframe target into numeric. Check your input. Continuing...') + print('Completed label encoding of target variable = %s' %each_target) + print('How model predictions need to be transformed for %s:\n\t%s' %(each_target, mlb.inverse_transformer)) + + ###################################################################################### + ###### B E F O R E U S I N G D A T A B U N C H C H E C K ################### + ###################################################################################### + ## Before using DataBunch check if certain encoders work with certain kind of data! + if feature_type: + final_cat_encoders = feature_type + else: + final_cat_encoders = [] + if settings.modeltype == 'Multi_Classification': + ### you must put a Polynomial Wrapper on the cat_encoder in case the model is multi-class + if final_cat_encoders: + final_cat_encoders = [PolynomialWrapper(x) for x in final_cat_encoders if x in settings.target_encoders_names] + elif settings.modeltype == 'Regression': + if final_cat_encoders: + if 'WOEEncoder' in final_cat_encoders: + print('Removing WOEEncoder from list of encoders since it cannot be used for this Regression problem.') + final_cat_encoders = [x for x in final_cat_encoders if x != 'WOEEncoder' ] + ###################################################################################### + ###### F E A T U R E E N G G U S I N G D A T A B U N C H ################### + ###################################################################################### + if feature_gen or feature_type: + if isinstance(test_data, str) or test_data is None: + print(' Starting feature engineering...Since no test data is given, splitting train into two...') + if settings.multi_label: + ### if it is a multi_label problem, leave target as it is - a list! + X_train, X_test, y_train, y_test = train_test_split(dataname[preds], + dataname[target], + test_size=0.2, + random_state=RANDOM_SEED) + else: + ### if it not a multi_label problem, make target as target[0] + X_train, X_test, y_train, y_test = train_test_split(dataname[preds], + dataname[target[0]], + test_size=0.2, + random_state=RANDOM_SEED) + else: + print(' Starting feature engineering...Since test data is given, using train and test...') + X_train = dataname[preds] + if settings.multi_label: + y_train = dataname[target] + else: + y_train = dataname[target[0]] + X_test = test_data[preds] + try: + y_test = test_data[target] + except: + y_test = None + X_train_index = X_train.index + X_test_index = X_test.index + + ################################################################################################## + ###### Category_Encoders does not work with Dask - so don't send in Dask dataframes to DataBunch! + ################################################################################################## + data_tuple = DataBunch(X_train=X_train, + y_train=y_train, + X_test=X_test, # be sure to specify X_test, because the encoder needs all dataset to work. + cat_features = catvars, + clean_and_encod_data=True, + cat_encoder_names=final_cat_encoders, # final list of Encoders selected + clean_nan=True, # fillnan + num_generator_features=np.where('interactions' in feature_gen,True, False).tolist(), # Generate interaction Num Features + group_generator_features=np.where('groupby' in feature_gen,True, False).tolist(), # Generate groupby Features + target_enc_cat_features=target_enc_cat_features,# Generate target encoded features + normalization=False, + random_state=RANDOM_SEED, + ) + #### Now you can process the tuple this way ######### + if type(y_train) == dask.dataframe.core.DataFrame: + ### since y_train is dask df and data_tuple.X_train is a pandas df, you can't merge them. + y_train = y_train.compute() ### remember you first have to convert them to a pandas df + data1 = pd.concat([data_tuple.X_train, y_train], axis=1) ### data_tuple does not have a y_train, remember! + + if isinstance(test_data, str) or test_data is None: + ### Since you have done a train_test_split using randomized split, you need to put it back again. + if type(y_test) == dask.dataframe.core.DataFrame: + ### since y_train is dask df and data_tuple.X_train is a pandas df, you can't merge them. + y_test = y_test.compute() ### remember you first have to convert them to a pandas df + data2 = pd.concat([data_tuple.X_test, y_test], axis=1) + dataname = data1.append(data2) + ### Sometimes there are duplicate values in index when you append. So just remove duplicates here + dataname = dataname[~dataname.index.duplicated()] + dataname = dataname.reindex(train_index) + print(' Completed feature engineering. Shape of Train (with target) = %s' %(dataname.shape,)) + else: + try: + if type(y_test) == dask.dataframe.core.DataFrame: + ### since y_train is dask df and data_tuple.X_train is a pandas df, you can't merge them. + y_test = y_test.compute() ### remember you first have to convert them to a pandas df + test_data = pd.concat([data_tuple.X_test, y_test], axis=1) + except: + test_data = copy.deepcopy(data_tuple.X_test) + ### Sometimes there are duplicate values in index when you append. So just remove duplicates here + test_data = test_data[~test_data.index.duplicated()] + test_data = test_data.reindex(test_index) + dataname = copy.deepcopy(data1) + print(' Completed feature engineering. Shape of Test (with target) = %s' %(test_data.shape,)) + ################################################################################################# + ###### Train and Test are currently pandas data frames even if dask_xgboost_flag is True ######## + ###### That is because we combined them after feature engg to using Category_Encoders ######## + ################################################################################################# + preds = [x for x in list(dataname) if x not in target] + numvars = dataname[preds].select_dtypes(include = 'number').columns.tolist() + if cat_vars: + #### if cat_vars input is given, use it! + catvars = copy.deepcopy(cat_vars) + numvars = left_subtract(preds, catvars) + else: + catvars = left_subtract(preds, numvars) + ###################### I M P O R T A N T ############################################## + important_cats = copy.deepcopy(catvars) + data_dim = int((len(dataname)*dataname.shape[1])/1e6) + ################################################################################################ + ############ S U L O V M E T H O D ############################### + #### If the data dimension is less than 5o Million then do SULOV - otherwise skip it! ######### + ################################################################################################ + + cols_with_infinity = EDA_find_remove_columns_with_infinity(dataname) + # first you must drop rows that have inf in them #### + if cols_with_infinity: + print('Dropped %d columns which contain infinity in dataset' %len(cols_with_infinity)) + #dataname = FE_drop_rows_with_infinity(dataname, cols_with_infinity, fill_value=True) + dataname = dataname.drop(cols_with_infinity, axis=1) + print('%s' %cols_with_infinity) + if isinstance(test_data,str) or test_data is None: + pass + else: + test_data = test_data.drop(cols_with_infinity, axis=1) + print(' dropped %s columns with infinity from test data...' %len(cols_with_infinity)) + numvars = left_subtract(numvars, cols_with_infinity) + print(' numeric features left = %s' %len(numvars)) + ####### This is where you start the SULOV process ################################## + start_time1 = time.time() + if len(numvars) > 1 and not skip_sulov: + if data_dim < 50: + try: + final_list = FE_remove_variables_using_SULOV_method(dataname,numvars,settings.modeltype,target, + corr_limit,verbose, dask_xgboost_flag) + except: + print(' SULOV method is erroring. Continuing ...') + final_list = copy.deepcopy(numvars) + else: + print(' Running SULOV on smaller dataset sample since data size %s m > 50 m. Continuing ...' %int(data_dim)) + if settings.modeltype != 'Regression': + data_temp = dataname.sample(n=10000, replace=True, random_state=99) + else: + data_temp = dataname[:10000] + final_list = FE_remove_variables_using_SULOV_method(data_temp,numvars,settings.modeltype,target, + corr_limit,verbose, dask_xgboost_flag) + del data_temp + elif skip_sulov: + print(' Skipping SULOV method. Continuing ...') + final_list = copy.deepcopy(numvars) + else: + print(' Skipping SULOV method since there are no continuous vars. Continuing ...') + final_list = copy.deepcopy(numvars) + ####### This is where you draw how featurewiz works when the verbose = 2 ########### + print('Time taken for SULOV method = %0.0f seconds' %(time.time()-start_time1)) + #### Now we create interaction variables between categorical features ######## + if verbose: + print(' Adding %s categorical variables to reduced numeric variables of %d' %( + len(important_cats),len(final_list))) + if isinstance(final_list,np.ndarray): + final_list = final_list.tolist() + preds = final_list+important_cats + if verbose and len(preds) <= 30: + print('Final list of selected %s vars after SULOV = %s' %(len(preds), preds)) + else: + print('Finally %s vars selected after SULOV' %(len(preds))) + #######You must convert category variables into integers ############### + print('Converting all features to numeric before sending to XGBoost...') + if isinstance(target, str): + dataname = dataname[preds+[target]] + else: + dataname = dataname[preds+target] + + if not test_data is None: + test_data = test_data[preds] + if len(important_cats) > 0: + dataname, test_data, error_columns = FE_convert_all_object_columns_to_numeric(dataname, test_data, preds) + important_cats = left_subtract(important_cats, error_columns) + if len(error_columns) > 0: + print(' removing %s object columns that could not be converted to numeric' %len(error_columns)) + preds = list(set(preds)-set(error_columns)) + dataname.drop(error_columns, axis=1, inplace=True) + else: + print(' there were no mixed data types or object columns that errored. Data is all numeric...') + print('Shape of train data after adding missing values flags = %s' %(dataname.shape,) ) + preds = [x for x in list(dataname) if x not in targets] + if not test_data is None: + if len(error_columns) > 0: + test_data.drop(error_columns, axis=1, inplace=True) + print(' Shape of test data after adding missing values flags = %s' %(test_data.shape,) ) + + if not skip_xgboost: + ### This is where we perform the recursive XGBoost method ############## + if verbose: + print('#######################################################################################') + print('##### R E C U R S I V E X G B O O S T : F E A T U R E S E L E C T I O N #######') + print('#######################################################################################') + + ################################################################################################# + ######## Now if dask_xgboost_flag is True, convert pandas dfs back to Dask Dataframes ##### + ################################################################################################# + if dask_xgboost_flag: + ### we reload the dataframes into dask since columns may have been dropped ## + if verbose: + print(' using DASK XGBoost') + train = load_dask_data(dataname, sep) + if not test_data is None: + test = load_dask_data(test_data, sep) + else: + ### we reload the dataframes into dask since columns may have been dropped ## + if verbose: + print(' using regular XGBoost') + train = copy.deepcopy(dataname) + test = copy.deepcopy(test_data) + ######## Conversion completed for train and test data ########## + #### If Category Encoding took place, these cat variables are no longer needed in Train. So remove them! + if feature_gen or feature_type: + print('Since %s category encoding is done, dropping original categorical vars from predictors...' %feature_gen) + preds = left_subtract(preds, catvars) + #### Now we process the numeric values through DASK XGBoost repeatedly ################### + start_time2 = time.time() + if dask_xgboost_flag: + important_features = FE_perform_recursive_xgboost(train, target, + settings.modeltype, settings.multi_label, dask_xgboost_flag, verbose) + else: + important_features = FE_perform_recursive_xgboost(dataname, target, + settings.modeltype, settings.multi_label, dask_xgboost_flag, verbose) + ###### E N D O F X G B O O S T S E L E C T I O N ############## + print(' Completed XGBoost feature selection in %0.0f seconds' %(time.time()-start_time2)) + if len(idcols) > 0: + print(' Alert: No ID variables %s are included in selected features' %idcols) + + else: + print('Skipping Recursive XGBoost method. Continuing ...') + important_features = copy.deepcopy(preds) + + if verbose: + print("#######################################################################################") + print("##### F E A T U R E S E L E C T I O N C O M P L E T E D #######") + print("#######################################################################################") + dicto = {} + missing_flags1 = [{x:x[:-13]} for x in important_features if 'Missing_Flag' in x] + for each_flag in missing_flags1: + print('Alert: Dont forget to add a missing flag to %s to create %s column' %(list(each_flag.values())[0], list(each_flag.keys())[0])) + dicto.update(each_flag) + if len(dicto) > 0: + important_features = [dicto.get(item,item) for item in important_features] + important_features = list(set(important_features)) + if len(important_features) <= 30: + print('Selected %d important features:\n%s' %(len(important_features), important_features)) + else: + print('Selected %d important features. Too many to print...' %len(important_features)) + numvars = [x for x in numvars if x in important_features] + important_cats = [x for x in important_cats if x in important_features] + print('Total Time taken for featurewiz selection = %0.0f seconds' %(time.time()-start_time)) + #### Now change the feature names back to original feature names ################ + item_replacer = col_name_mapper.get # For faster gets. + ########################################################################## + ### You select the features with the same old names as before here ####### + ########################################################################## + ## In one case, column names get changed in train but not in test since it test is not available. + if isinstance(test_data, str) or test_data is None: + print('Output contains a list of %s important features and a train dataframe' %len(important_features)) + else: + print('Output contains two dataframes: train and test with %d important features.' %len(important_features)) + if feature_gen or feature_type: + if isinstance(test_data, str) or test_data is None: + ### if feature engg is performed, id columns are dropped. Hence they must rejoin here. + dataname = pd.concat([train_ids, dataname], axis=1) + if isinstance(target, str): + return important_features, dataname[important_features+[target]] + else: + return important_features, dataname[important_features+target] + else: + ### if feature engg is performed, id columns are dropped. Hence they must rejoin here. + dataname = pd.concat([train_ids, dataname], axis=1) + test_data = pd.concat([test_ids, test_data], axis=1) + if isinstance(target, str): + return dataname[important_features+[target]], test_data[important_features] + else: + return dataname[important_features+target], test_data[important_features] + else: + ### You select the features with the same old names as before ####### + old_important_features = copy.deepcopy(important_features) + if len(date_cols) > 0: + date_replacer = date_col_mappers.get # For faster gets. + important_features1 = [date_replacer(n, n) for n in important_features] + else: + important_features1 = [item_replacer(n, n) for n in important_features] + important_features = find_remove_duplicates(important_features1) + if old_important_features == important_features: + ## Don't drop the old target since there is only one target here ### + pass + else: + if len(old_important_features) == len(important_features): + ### You just move the values from the new names to the old feature names ## + dataname[important_features] = dataname[old_important_features] + if isinstance(test_data, str) or test_data is None: + pass + else: + #### if there is test data transfer values to it ### + test_data[important_features] = test_data[old_important_features] + else: + ### first try to return with the new important features, if that fails return with old features + try: + print('There are special chars in column names. Please remove them and try again.') + if isinstance(test_data, str) or test_data is None: + return important_features, dataname[important_features] + else: + return dataname[important_features], test_data[important_features] + except: + print('There are special chars in column names. Returning with important features and train.') + if isinstance(test_data, str) or test_data is None: + return old_important_features, dataname[old_important_features] + else: + return dataname[old_important_features], test_data[old_important_features] + + old_target = copy.deepcopy(target) + if isinstance(target, str): + target = item_replacer(target, target) + targets = [target] + else: + target = [item_replacer(n, n) for n in target] + targets = copy.deepcopy(target) + + if old_target == target: + ## Don't drop the old target since there is only one target here ### + pass + else: + ### you don't need drop the cols that have changed since only a few are selected ####### + if isinstance(target, str): + dataname[target] = dataname[old_target] + else: + copy_targets = copy.deepcopy(targets) + copy_old = copy.deepcopy(old_target) + for each_target, each_old_target in zip(copy_targets, copy_old): + dataname[each_target] = dataname[each_old_target] + + #### This is where we check whether to return test data or not ###### + try: + if isinstance(test_data, str) or test_data is None: + if feature_gen or feature_type: + ### if feature engg is performed, id columns are dropped. Hence they must rejoin here. + dataname = pd.concat([train_ids, dataname], axis=1) + return important_features, dataname[important_features+target] + else: + ## This is for test data existing #### + if feature_gen or feature_type: + ### if feature engg is performed, id columns are dropped. Hence they must rejoin here. + dataname = pd.concat([train_ids, dataname], axis=1) + test_data = pd.concat([test_ids, test_data], axis=1) + ### You select the features with the same old names as before ####### + return dataname[important_features+targets], test_data[important_features] + except: + print('Warning: Returning with important features and train. Please re-check your outputs.') + return important_features, dataname[important_features+targets] +################################################################################ +def FE_perform_recursive_xgboost(train, target, model_type, multi_label_type, + dask_xgboost_flag=False, verbose=0): + """ + Perform recursive XGBoost to identify most important features in an all- + numeric dataset. If the dataset is not numeric, it will give an error. + + Inputs: + X_train: a pandas dataframe containing all-numeric features + y_train: a pandas Series or Dataframe containing all-numeric features. + model_type: can be one of "Classification" or "Regression". If it is + multi-class, you can also use 'Multi-Classification" as input. + multi_label_type: can be one of "Single_Label" or "Multi_Label". + dask_xgboost_flag: False by default. You can set it to True if your dataset + is a Dask dataframe and Series. + + output: + features: a list of top features in dataset. + + """ + ### TO DO: target can be multi-label - need to test for it + ### TO DO: train can be DASK dataframe - need to test for it + ### we need to use the name train_p to start and then change it to X_train + train_p = train.drop(target, axis=1) + ### train is already a dask dataframe -> you can leave it as it is + y_train = train[target] + cpu_tree_method = 'hist' + cols_sel = train_p.columns.tolist() + ######## Do the Dask settings here ####### + if dask_xgboost_flag: + if verbose: + print(' Dask version = %s' %dask.__version__) + ### You can remove dask_ml here since you don't do any split of train test here ########## + #from dask_ml.model_selection import train_test_split + ### check available memory and allocate at least 1GB of it in the Client in DASK ######### + n_workers = get_cpu_worker_count() + ### Avoid reducing the free memory - leave it as big as it wants to be ### + memory_free = str(max(1, int(psutil.virtual_memory()[0]/(n_workers*1e9))))+'GB' + print(' Using Dask XGBoost algorithm with %s virtual CPUs and %s memory limit...' %( + get_cpu_worker_count(), memory_free)) + client = Client(n_workers= n_workers, threads_per_worker=1, processes=True, silence_logs=50, + memory_limit=memory_free) + print('Dask client configuration: %s' %client) + import gc + client.run(gc.collect) + #train_p = client.persist(train_p) + important_features = [] + ####################################################################### + ##### This is for DASK XGB Regressor and XGB Classifier problems #### + ####################################################################### + if settings.multi_label: + ### only regular xgboost with multi-output can work well in multi-label settings # + dask_xgboost_flag = False + bst_models = [] + + ######### This is for DASK Dataframes XGBoost training #################### + try: + xgb.set_config(verbosity=0) + except: + ## Some cases, this errors, so pass ### + pass + #### Limit the number of iterations here ###### + if train_p.shape[1] <= 10: + iter_limit = 2 + else: + iter_limit = int(train_p.shape[1]/5+0.5) + ###################### I M P O R T A N T ############################################## + ###### This top_num decides how many top_n features XGB selects in each iteration. + #### There a total of 5 iterations. Hence 5x10 means maximum 50 features will be selected. + ##### If there are more than 50 variables, then maximum 25% of its variables will be selected + if len(cols_sel) <= 50: + #top_num = 10 + top_num = int(max(2, len(cols_sel)*0.25)) + else: + ### the maximum number of variables will be 25% of preds which means we divide by 4 and get 25% here + ### The five iterations result in 10% being chosen in each iteration. Hence max 50% of variables! + top_num = int(len(cols_sel)*0.20) + if verbose: + print(' Taking top %s features per iteration...' %top_num) + try: + for i in range(0,train_p.shape[1],iter_limit): + start_time1 = time.time() + imp_feats = [] + if train_p.shape[1]-i < iter_limit: + X_train = train_p.iloc[:,i:] + cols_sel = X_train.columns.tolist() + else: + X_train = train_p[list(train_p.columns.values)[i:train_p.shape[1]]] + cols_sel = X_train.columns.tolist() + ##### This is where you repeat the training and finding feature importances + if dask_xgboost_flag: + rows = X_train.compute().shape[0] + else: + rows = X_train.shape[0] + if rows >= 100000: + num_rounds = 20 + else: + num_rounds = 100 + if i == 0: + if verbose: + print(' Number of booster rounds = %s' %num_rounds) + if train_p.shape[1]-i < 2: + ### If there is just one variable left, then just skip it ##### + continue + #### The target must always be numeric ## + if model_type == 'Regression': + objective = 'reg:squarederror' + params = {'objective': objective, + "silent":True, "verbosity": 0, 'min_child_weight': 0.5} + else: + #### This is for Classifiers only ########## + if model_type == 'Binary_Classification': + objective = 'binary:logistic' + num_class = 1 + params = {'objective': objective, 'num_class': num_class, + "silent":True, "verbosity": 0, 'min_child_weight': 0.5} + else: + objective = 'multi:softmax' + try: + ### This is in case target is a list ### + num_class = train[target].nunique()[0] + except: + ### This is in case target is a string ### + num_class = train[target].nunique() + params = {'objective': objective, + "silent":True, "verbosity": 0, 'min_child_weight': 0.5, 'num_class': num_class} + ############################################################################################################ + ######### This is where we find out whether to use single or multi-label for xgboost ####################### + ############################################################################################################ + if multi_label_type: + if model_type == 'Regression': + clf = XGBRegressor(n_jobs=-1, n_estimators=100, max_depth=4, random_state=99) + clf.set_params(**params) + bst = MultiOutputRegressor(clf) + else: + clf = XGBClassifier(n_jobs=-1, n_estimators=100, max_depth=4, random_state=99) + clf.set_params(**params) + bst = MultiOutputClassifier(clf) + bst.fit(X_train, y_train) + else: + if not dask_xgboost_flag: + ################################################################################ + ######### Training Regular XGBoost on pandas dataframes only ################## + ################################################################################ + #### now this training via bst works well for both xgboost 0.0.90 as well as 1.5.1 ## + #print('cols order: ',cols_sel) + try: + dtrain = xgb.DMatrix(X_train, y_train, enable_categorical=True, feature_names=cols_sel) + bst = xgb.train(params, dtrain, num_boost_round=num_rounds) + + except Exception as error_msg: + + print('Regular XGBoost is crashing due to: %s' %error_msg) + if model_type == 'Regression': + params = {'tree_method': cpu_tree_method, 'gpu_id': None} + else: + params = {'tree_method': cpu_tree_method,'num_class': num_class, 'gpu_id': None} + print(error_msg) + bst = xgb.train(params, dtrain, num_boost_round=num_rounds) + else: + ################################################################################ + ########## Training XGBoost model using dask_xgboost ######################### + ################################################################################ + ### the dtrain syntax can only be used xgboost 1.50 or greater. Dont use it until then. + ### use the next line for new xgboost version 1.5.1 abd higher ######### + try: + #### SYNTAX BELOW WORKS WELL. BUT YOU CANNOT DO EVALS WITH DASK XGBOOST AS OF NOW #### + #dtrain = xgb.dask.DaskDMatrix(client, X_train, y_train) + #bst = xgb.dask.train(client, params, dtrain, num_boost_round=num_rounds) + bst = dask_xgboost_training(X_train, y_train, params) + except Exception as error_msg: + if model_type == 'Regression': + params = {'tree_method': cpu_tree_method} + else: + params = {'tree_method': cpu_tree_method,'num_class': num_class} + dtrain = xgb.DMatrix(X_train, label=y_train, feature_names=cols_sel) + bst = xgb.dask.train(client=client, params=params, dtrain=dtrain, num_boost_round=num_rounds) + print(error_msg) + ################################################################################ + if not dask_xgboost_flag: + bst_models.append(bst) + else: + bst_models.append(bst['booster']) + ##### to get the params of an xgboost booster object you have to do the following steps: + if verbose >= 3: + if not dask_xgboost_flag : + print('Regular XGBoost model parameters:\n') + config = json.loads(bst.save_config()) + else: + print('Dask XGBoost model parameters:\n') + boo = bst['booster'] + config = json.loads(boo.save_config()) + print(config) + #### use this next one for dask_xgboost old ############### + if multi_label_type: + imp_feats = dict(zip(X_train.columns, bst.estimators_[0].feature_importances_)) + else: + if not dask_xgboost_flag: + imp_feats = bst.get_score(fmap='', importance_type='total_gain') + else: + imp_feats = bst['booster'].get_score(fmap='', importance_type='total_gain') + ### skip the next statement since it is duplicating the work of sort_values ## + #imp_feats = dict(sorted(imp_feats.items(),reverse=True, key=lambda item: item[1])) + ### doing this for single-label is a little different from multi_label_type ######### + + #imp_feats = model_xgb.get_booster().get_score(importance_type='gain') + #print('%d iteration: imp_feats = %s' %(i+1,imp_feats)) + if len(pd.Series(imp_feats)[pd.Series(imp_feats).sort_values(ascending=False)/pd.Series(imp_feats).values.max()>=0.5]) > 1: + print_feats = (pd.Series(imp_feats)[pd.Series(imp_feats).sort_values(ascending=False)/pd.Series(imp_feats).values.max()>=0.5]).index.tolist() + if len(print_feats) < top_num: + print_feats = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].index.tolist() + if len(print_feats) <= 30 and verbose: + print(' Selected: %s' %print_feats) + important_features += print_feats + else: + print_feats = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].index.tolist() + if len(print_feats) <= 30 and verbose: + print(' Selected: %s' %pd.Series(imp_feats).sort_values(ascending=False)[:top_num].index.tolist()) + important_features += print_feats + ####### order this in the same order in which they were collected ###### + important_features = list(OrderedDict.fromkeys(important_features)) + if dask_xgboost_flag: + if verbose >= 2: + print(' Time taken for DASK XGBoost feature selection = %0.0f seconds' %(time.time()-start_time1)) + else: + if verbose >= 2: + print(' Time taken for regular XGBoost feature selection = %0.0f seconds' %(time.time()-start_time1)) + #### plot all the feature importances in a grid ########### + + if verbose >= 2: + if multi_label_type: + draw_feature_importances_multi_label(bst_models, dask_xgboost_flag) + else: + draw_feature_importances_single_label(bst_models, dask_xgboost_flag) + except Exception as e: + if dask_xgboost_flag: + print('Dask XGBoost is crashing due to %s. Returning with currently selected features...' %e) + else: + print('Regular XGBoost is crashing due to %s. Returning with currently selected features...' %e) + important_features = copy.deepcopy(cols_sel) + return important_features +################################################################################ +def remove_highly_correlated_vars_fast(df, corr_limit=0.70): + """ + This is a simple method to remove highly correlated features fast using Pearson's Correlation. + Use this only for float and integer variables. It will automatically select those only. + It can be used for very large data sets where featurewiz has trouble with memory + """ + # Creating correlation matrix + correlation_dataframe = df.corr().abs().astype(np.float16) + # Selecting upper triangle of correlation matrix + upper_tri = correlation_dataframe.where(np.triu(np.ones(correlation_dataframe.shape), + k=1).astype(np.bool)) + # Finding index of feature columns with correlation greater than 0.95 + to_drop = [column for column in upper_tri.columns if any(upper_tri[column] > corr_limit)] + print(); + print('Highly correlated columns to remove: %s' %to_drop) + return to_drop +##################################################################################### +import multiprocessing +def get_cpu_worker_count(): + return multiprocessing.cpu_count() +############################################################################################# +from itertools import combinations +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +from sklearn.feature_selection import chi2, mutual_info_regression, mutual_info_classif +from sklearn.feature_selection import SelectKBest +import xgboost +def draw_feature_importances_multi_label(bst_models, dask_xgboost_flag=False): + rows = int(len(bst_models)/2 + 0.5) + colus = 2 + fig, ax = plt.subplots(rows, colus) + fig.set_size_inches(min(colus*5,20),rows*5) + fig.subplots_adjust(hspace=0.5) ### This controls the space betwen rows + fig.subplots_adjust(wspace=0.5) ### This controls the space between columns + counter = 0 + if rows == 1: + ax = ax.reshape(-1,1).T + for k in np.arange(rows): + for l in np.arange(colus): + if counter < len(bst_models): + try: + bst_booster = bst_models[counter].estimators_[0] + ax1 = xgboost.plot_importance(bst_booster, height=0.8, show_values=False, + importance_type='gain', max_num_features=10, ax=ax[k][l]) + ax1.set_title('Multi_label: Top 10 features for first label: round %s' %(counter+1)) + except: + pass + counter += 1 + plt.show(); +######################################################################################## +def draw_feature_importances_single_label(bst_models, dask_xgboost_flag=False): + rows = int(len(bst_models)/2 + 0.5) + colus = 2 + fig, ax = plt.subplots(rows, colus) + fig.set_size_inches(min(colus*5,20),rows*5) + fig.subplots_adjust(hspace=0.5) ### This controls the space betwen rows + fig.subplots_adjust(wspace=0.5) ### This controls the space between columns + counter = 0 + if rows == 1: + ax = ax.reshape(-1,1).T + for k in np.arange(rows): + for l in np.arange(colus): + if counter < len(bst_models): + try: + bst_booster = bst_models[counter] + ax1 = xgboost.plot_importance(bst_booster, height=0.8, show_values=False, + importance_type='gain', max_num_features=10, ax=ax[k][l]) + ax1.set_title('Top 10 features with XGB model %s' %(counter+1)) + except: + pass + counter += 1 + plt.show(); +###################################################################################### +def reduce_mem_usage(df): + """ + ##################################################################### + Greatly indebted to : + https://www.kaggle.com/arjanso/reducing-dataframe-memory-size-by-65 + for this function to reduce memory usage. + ##################################################################### + It is a bit slow as it iterates through all the columns of a dataframe and modifies data types + to reduce memory usage. But it has been shown to reduce memory usage by 65% or so. + """ + start_mem = df.memory_usage().sum() / 1024**2 + if type(df) == dask.dataframe.core.DataFrame: + start_mem = start_mem.compute() + print(' Caution: We will try to reduce the memory usage of dataframe from {:.2f} MB'.format(start_mem)) + cols = df.columns + if type(df) == dask.dataframe.core.DataFrame: + cols = cols.tolist() + + for col in cols: + col_type = df[col].dtype + if col_type != object: + try: + c_min = df[col].min() + c_max = df[col].max() + except: + continue + if type(df) == dask.dataframe.core.DataFrame: + c_min = c_min.compute() + c_max = c_max.compute() + if str(col_type)[:3] == 'int': + if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max: + df[col] = df[col].astype(np.int8) + elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max: + df[col] = df[col].astype(np.int16) + elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max: + df[col] = df[col].astype(np.int32) + elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max: + df[col] = df[col].astype(np.int64) + else: + try: + if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max: + df[col] = df[col].astype(np.float16) + elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max: + df[col] = df[col].astype(np.float32) + else: + df[col] = df[col].astype(np.float64) + except: + continue + else: + df[col] = df[col].astype('category') + + ####### Results after memory usage function ################### + end_mem = df.memory_usage().sum() / 1024**2 + if type(df) == dask.dataframe.core.DataFrame: + end_mem = end_mem.compute() + print(f' by {(100 * (start_mem - end_mem) / start_mem):.1f}%. Memory usage after is: {end_mem:.2f} MB') + return df +################################################################################## +def FE_start_end_date_time_features(smalldf, startTime, endTime, splitter_date_string="/",splitter_hour_string=":"): + """ + FE stands for Feature Engineering - it means this function performs feature engineering + ###################################################################################### + This function is used when you have start and end date time stamps in your dataset. + - If there is no start and end time features, don't use it. Both must be present! + - this module will create additional features for such fields. + - you must provide a start date time stamp field and an end date time stamp field + Otherwise, you are better off using the FE_create_date_time_features() module in this library. + + Inputs: + smalldf: Dataframe containing your date time fields + startTime: this is hopefully a string field which converts to a date time stamp easily. Make sure it is a string. + endTime: this also must be a string field which converts to a date time stamp easily. Make sure it is a string. + splitter_date_string: usually there is a string such as '/' or '.' between day/month/year etc. Default is assumed / here. + splitter_hour_string: usually there is a string such as ':' or '.' between hour:min:sec etc. Default is assumed : here. + + Outputs: + The original pandas dataframe with additional fields created by splitting the start and end time fields + ###################################################################################### + """ + smalldf = smalldf.copy() + add_cols = [] + date_time_variable_flag = False + if smalldf[startTime].dtype in ['datetime64[ns]','datetime16[ns]','datetime32[ns]']: + print('%s variable is a date-time variable' %startTime) + date_time_variable_flag = True + if date_time_variable_flag: + view_days = 'processing'+startTime+'_elapsed_days' + smalldf[view_days] = (smalldf[endTime] - smalldf[startTime]).astype('timedelta64[s]')/(60*60*24) + smalldf[view_days] = smalldf[view_days].astype(int) + add_cols.append(view_days) + view_time = 'processing'+startTime+'_elapsed_time' + smalldf[view_time] = (smalldf[endTime] - smalldf[startTime]).astype('timedelta64[s]').values + add_cols.append(view_time) + else: + start_date = 'processing'+startTime+'_start_date' + smalldf[start_date] = smalldf[startTime].map(lambda x: x.split(" ")[0]) + add_cols.append(start_date) + try: + start_time = 'processing'+startTime+'_start_time' + smalldf[start_time] = smalldf[startTime].map(lambda x: x.split(" ")[1]) + add_cols.append(start_time) + except: + ### there is no hour-minutes part of this date time stamp field. You can just skip it if it is not there + pass + end_date = 'processing'+endTime+'_end_date' + smalldf[end_date] = smalldf[endTime].map(lambda x: x.split(" ")[0]) + add_cols.append(end_date) + try: + end_time = 'processing'+endTime+'_end_time' + smalldf[end_time] = smalldf[endTime].map(lambda x: x.split(" ")[1]) + add_cols.append(end_time) + except: + ### there is no hour-minutes part of this date time stamp field. You can just skip it if it is not there + pass + view_days = 'processing'+startTime+'_elapsed_days' + smalldf[view_days] = (pd.to_datetime(smalldf[end_date]) - pd.to_datetime(smalldf[start_date])).values.astype(int) + add_cols.append(view_days) + try: + view_time = 'processing'+startTime+'_elapsed_time' + smalldf[view_time] = (pd.to_datetime(smalldf[end_time]) - pd.to_datetime(smalldf[start_time])).astype('timedelta64[s]').values + add_cols.append(view_time) + except: + ### In some date time fields this gives an error so skip it in that case + pass + #### The reason we chose endTime here is that startTime is usually taken care of by another library. So better to do this alone. + year = 'processing'+endTime+'_end_year' + smalldf[year] = smalldf[end_date].map(lambda x: str(x).split(splitter_date_string)[0]).values + add_cols.append(year) + #### The reason we chose endTime here is that startTime is usually taken care of by another library. So better to do this alone. + month = 'processing'+endTime+'_end_month' + smalldf[month] = smalldf[end_date].map(lambda x: str(x).split(splitter_date_string)[1]).values + add_cols.append(month) + try: + #### The reason we chose endTime here is that startTime is usually taken care of by another library. So better to do this alone. + daynum = 'processing'+endTime+'_end_day_number' + smalldf[daynum] = smalldf[end_date].map(lambda x: str(x).split(splitter_date_string)[2]).values + add_cols.append(daynum) + except: + ### In some date time fields the day number is not there. If not, just skip it #### + pass + #### In some date time fields, the hour and minute is not there, so skip it in that case if it errors! + try: + start_hour = 'processing'+startTime+'_start_hour' + smalldf[start_hour] = smalldf[start_time].map(lambda x: str(x).split(splitter_hour_string)[0]).values + add_cols.append(start_hour) + start_min = 'processing'+startTime+'_start_hour' + smalldf[start_min] = smalldf[start_time].map(lambda x: str(x).split(splitter_hour_string)[1]).values + add_cols.append(start_min) + except: + ### If it errors, skip it + pass + #### Check if there is a weekday and weekends in date time columns using endTime only + weekday_num = 'processing'+endTime+'_end_weekday_number' + smalldf[weekday_num] = pd.to_datetime(smalldf[end_date]).dt.weekday.values + add_cols.append(weekday_num) + weekend = 'processing'+endTime+'_end_weekend_flag' + smalldf[weekend] = smalldf[weekday_num].map(lambda x: 1 if x in[5,6] else 0) + add_cols.append(weekend) + #### If everything works well, there should be 13 new columns added by module. All the best! + print('%d columns added using start date=%s and end date=%s processing...' %(len(add_cols),startTime,endTime)) + return smalldf +########################################################################### +def FE_split_one_field_into_many(df_in, field, splitter, filler, new_names_list='', add_count_field=False): + """ + FE stands for Feature Engineering - it means this function performs feature engineering + ###################################################################################### + This function takes any data frame field (string variables only) and splits + it into as many fields as you want in the new_names_list. + + Inputs: + dft: pandas DataFrame + field: name of string column that you want to split using the splitter string specified + splitter: specify what string to split on using the splitter argument. + filler: You can also fill Null values that may happen due to your splitting by specifying a filler. + new_names_list: If no new_names_list is given, then we use the name of the field itself to create new columns. + add_count_field: False (default). If True, it will count the number of items in + the "field" column before the split. This may be needed in nested dictionary fields. + + Outputs: + dft: original dataframe with additional columns created by splitting the field. + new_names_list: the list of new columns created by this function + ###################################################################################### + """ + df_field = df_in[field].values + df = copy.deepcopy(df_in) + ### First copy whatever is in that field so we can save it for later ### + ### Remember that fillna only works at dataframe level! ### + df[[field]] = df[[field]].fillna(filler) + if add_count_field: + ### there will be one extra field created when we count the number of contents in each field ### + max_things = df[field].map(lambda x: len(x.split(splitter))).max() + 1 + else: + max_things = df[field].map(lambda x: len(x.split(splitter))).max() + if len(new_names_list) == 0: + print(' Max. columns created by splitting %s field is %d.' %( + field,max_things)) + else: + if not max_things == len(new_names_list): + print(""" Max. columns created by splitting %s field is %d but you have given %d + variable names only. Selecting first %d""" %( + field,max_things,len(new_names_list),len(new_names_list))) + ### This creates a new field that counts the number of things that are in that field. + if add_count_field: + #### this counts the number of contents after splitting each row which varies. Hence it helps. + num_products_viewed = 'Content_Count_in_'+field + df[num_products_viewed] = df[field].map(lambda x: len(x.split(splitter))).values + ### Clean up the field such that it has the right number of split chars otherwise add to it + ### This fills up the field with empty strings between each splitter. You can't do much about it. + #### Leave this as it is. It is not something you can do right now. It works. + fill_string = splitter + filler + df[field] = df[field].map(lambda x: x+fill_string*(max_things-len(x.split(splitter))) if len( + x.split(splitter)) < max_things else x) + ###### Now you create new fields by split the one large field ######## + if isinstance(new_names_list, str): + if new_names_list == '': + new_names_list = [field+'_'+str(i) for i in range(1,max_things+1)] + else: + new_names_list = [new_names_list] + ### First fill empty spaces or NaNs with filler ### + df.loc[df[field] == splitter, field] = filler + for i in range(len(new_names_list)): + try: + df[new_names_list[i]] = df[field].map(lambda x: x.split(splitter)[i] + if splitter in x else filler) + except: + df[new_names_list[i]] = filler + continue + ### there is really nothing you can do to fill up since they are filled with empty strings. + #### Leave this as it is. It is not something you can do right now. It works. + df[field] = df_field + return df, new_names_list +########################################################################### +def FE_add_groupby_features_aggregated_to_dataframe(train, + agg_types, groupby_columns, ignore_variables, test=""): + """ + FE stands for Feature Engineering. This function performs feature engineering on data. + ###################################################################################### + ### This function is a very fast function that will compute aggregates for numerics + ### It returns original dataframe with added features from numeric variables aggregated + ### What do you mean aggregate? aggregates can be "count, "mean", "median", etc. + ### What do you aggregrate? all numeric columns in your data + ### What do you groupby? one groupby column at a time or multiple columns one by one + ### -- if you give it a list of columns, it will execute the grouping one by one + ### What is the ignore_variables for? it will ignore these variables from grouping. + ### Make sure to reduce correlated features using FE_remove_variables_using_SULOV_method() + ###################################################################################### + ### Inputs: + ### train: Just sent in the data frame where you want aggregated features for. + ### agg_types: list of computational types: 'mean','median','count', + ### 'max', 'min', 'sum', etc. + ### One caveat: these agg_types must be found in the following agg_func of + ### numpy or pandas groupby statement. + ### List of aggregates available: {'count','sum','mean','mad','median','min','max', + ### 'mode','abs', 'prod','std','var','sem','skew','kurt', + ### 'quantile','cumsum','cumprod','cummax','cummin'} + ### groupby_columns: can be a string representing a single column or a list of + ### multiple columns + ### - it will groupby all the numeric features using one groupby column + ### at a time in a loop. + ### ignore_variables: list of variables to ignore among numeric variables in + ### data since they may be ID variables. + ### Outputs: + ### Returns the original dataframe with additional features created by this function. + ###################################################################################### + """ + trainx = copy.deepcopy(train) + testx = copy.deepcopy(test) + if isinstance(groupby_columns, str): + groupby_columns = [groupby_columns] + numerics = trainx.select_dtypes(include='number').columns.tolist() + numerics = [x for x in numerics if x not in ignore_variables] + MGB = Groupby_Aggregator(categoricals=groupby_columns, + aggregates=agg_types, numerics=numerics) + train_copy = MGB.fit_transform(trainx) + if isinstance(testx, str) or testx is None: + test_copy = testx + else: + test_copy = MGB.transform(testx) + ### return the dataframes ########### + return train_copy, test_copy +##################################################################################################### +def FE_combine_rare_categories(train_df, categorical_features, test_df=""): + """ + In this function, we will select all rare classes having representation <1% of population and + group them together under a new label called 'RARE'. We will apply this on train and test (optional) + """ + train_df = copy.deepcopy(train_df) + test_df = copy.deepcopy(test_df) + train_df[categorical_features] = train_df[categorical_features].apply( + lambda x: x.mask(x.map(x.value_counts())< (0.01*train_df.shape[0]), 'RARE')) + for col in categorical_features: + vals = list(train_df[col].unique()) + if isinstance(test_df, str) or test_df is None: + return train_df, test_df + else: + test_df[col] = test_df[col].apply(lambda x: 'RARE' if x not in vals else x) + return train_df, test_df + +##################################################################################################### +def FE_get_latest_values_based_on_date_column(dft, id_col, date_col, cols, ascending=False): + """ + FE means FEATURE ENGINEERING - That means this function will create new features + ###################################################################################### + This function gets you the latest values of the columns in cols from a date column date_col. + + Inputs: + dft: dataframe, pandas + id_col: you need to provide an ID column to groupby the cols and then sort them by date_col. + date_col: this must be a valid pandas date-time column. If it is a string column, + make sure you change it to a date-time column. + It sorts each group by the latest date (descending) and selects that top row. + cols: these are the list of columns you want their latest value based on the date-col you specify. + These cols can be any type of column: numeric or string. + ascending: Set this as True or False depending on whether you want smallest or biggest on top. + + Outputs: + Returns a dataframe that is smaller than input dataframe since it groups cols by ID_column. + ###################################################################################### + Beware! You will get a dataframe that has fewer cols than your input with fewer rows than input. + """ + dft = copy.deepcopy(dft) + try: + if isinstance(cols, str): + cols = [cols] + train_add = dft.groupby([id_col], sort=False).apply(lambda x: x.sort_values([date_col], + ascending=ascending)) + train_add = train_add[cols].reset_index() + train_add = train_add.groupby(id_col).head(1).reset_index(drop=True).drop('level_1',axis=1) + except: + print(' Error in getting latest status of columns based on %s. Returning...' %date_col) + return dft + return train_add +################################################################################# +from functools import reduce +def FE_split_add_column(dft, col, splitter=',', action='add'): + """ + FE means FEATURE ENGINEERING - That means this function will create new features + ###################################################################################### + This function will split a column's values based on a splitter you specify and + will either add them or concatenate them as you specify in the action argument. + + Inputs: + dft: pandas DataFrame + col: name of column that you want to split into its constituent parts. It must be a string column. + splitter: splitter can be any string that is found in your column and that you want to split by. + action: can be any one of following: {'add', 'subtract', 'multiply', 'divide', 'concat', 'concatenate'} + ################################################################################ + Returns a dataframe with a new column that is a modification of the old column + """ + dft = copy.deepcopy(dft) + new_col = col + '_split_apply' + print('Creating column = %s using split_add feature engineering...' %new_col) + if action in ['+','-','*','/','add','subtract','multiply','divide']: + if action in ['add','+']: + sign = '+' + elif action in ['-', 'subtract']: + sign = '-' + elif action in ['*', 'multiply']: + sign = '*' + elif action in ['/', 'divide']: + sign = '/' + else: + sign = '+' + # using reduce to compute sum of list + try: + trainx = dft[col].astype(str) + trainx = trainx.map(lambda x: 0 if x is np.nan else 0 if x == '' else x.split(splitter)).map( + lambda listx: [int(x) if x != '' else 0 for x in listx ] if isinstance(listx,list) else [0,0]) + dft[new_col] = trainx.map(lambda lis: reduce(lambda a,b : eval('a'+sign+'b'), lis) if isinstance(lis,list) else 0).values + except: + print(' Error: returning without creating new column') + return dft + elif action in ['concat','concatenate']: + try: + dft[new_col] = dft[col].map(lambda x: " " if x is np.nan else " " if x == '' else x.split(splitter)).map( + lambda listx: np.concatenate([str(x) if x != '' else " " for x in listx] if isinstance(listx,list) else " ")).values + except: + print(' Error: returning without creating new column') + else: + print('Could not perform action. Please check your inputs and try again') + return dft + return dft +################################################################################ +def FE_add_age_by_date_col(dft, date_col, age_format): + """ + FE means FEATURE ENGINEERING - That means this function will create new features + ###################################################################################### + This handy function gets you age from the date_col to today. It can be counted in months or years or days. + ###################################################################################### + It returns the same dataframe with an extra column added that gives you age + """ + if not age_format in ['M','D','Y']: + print('Age is not given in right format. Must be one of D, Y or M') + return dft + new_date_col = 'last_'+date_col+'_in_months' + try: + now = pd.Timestamp('now') + dft[date_col] = pd.to_datetime(dft[date_col], format='%y-%m-%d') + dft[date_col] = dft[date_col].where(dft[date_col] < now, dft[date_col] - np.timedelta64(100, age_format)) + if age_format == 'M': + dft[new_date_col] = (now - dft[date_col]).astype(' upper_limit )].index + + ### Capping using the n largest value based on n given in input. + maxval = df[col].max() ## what is the maximum value in this column? + num_maxs = df[df[col]==maxval].shape[0] ## number of rows that have max value + ### find the n_largest values after the maximum value based on given input n + num_largest_after_max = num_maxs + cap_at_nth_largest + capped_value = df[col].nlargest(num_largest_after_max).iloc[-1] ## this is the value we cap it against + df.loc[df[col]==maxval, col] = capped_value ## maximum values are now capped + ### you are now good to go - you can show how they are capped using before and after pics + if verbose: + df[col].plot(kind='box', title = '%s after capping outliers' %col, ax=ax2) + plt.show() + + # Let's save the list of outliers and see if there are some with outliers in multiple columns + outlier_indices.extend(outlier_list_col) + + # select certain observations containing more than one outlier in 2 columns or more. We can drop them! + outlier_indices = Counter(outlier_indices) + multiple_outliers = list( k for k, v in outlier_indices.items() if v > 3 ) + ### now drop these rows altogether #### + if drop: + print('Shape of dataframe before outliers being dropped: %s' %(df.shape,)) + number_of_rows = df.shape[0] + df = df.drop(multiple_outliers, axis=0) + print('Shape of dataframe after outliers being dropped: %s' %(df.shape,)) + print('\nNumber_of_rows with multiple outliers in more than 3 columns which were dropped = %d' %(number_of_rows-df.shape[0])) + return df +################################################################################# +def EDA_classify_and_return_cols_by_type(df1): + """ + EDA stands for Exploratory data analysis. This function performs EDA - hence the name + ######################################################################################## + This handy function classifies your columns into different types : make sure you send only predictors. + Beware sending target column into the dataframe. You don't want to start modifying it. + ##################################################################################### + It returns a list of categorical columns, integer cols and float columns in that order. + """ + ### Let's find all the categorical excluding integer columns in dataset: unfortunately not all integers are categorical! + catcols = df1.select_dtypes(include='object').columns.tolist() + df1.select_dtypes(include='category').columns.tolist() + cats = copy.deepcopy(catcols) + nlpcols = [] + for each_cat in cats: + try: + if df1[[each_cat]].fillna('missing').map(len).mean() >= 40: + nlpcols.append(each_cat) + catcols.remove(each_cat) + except: + continue + intcols = df1.select_dtypes(include='integer').columns.tolist() + # let's find all the float numeric columns in data + floatcols = df1.select_dtypes(include='float').columns.tolist() + return catcols, intcols, floatcols, nlpcols +############################################################################################ +def EDA_classify_features_for_deep_learning(train, target, idcols): + """ + ###################################################################################### + This is a simple method of classifying features into 4 types: cats, integers, floats and NLPs + This is needed for deep learning problems where we need fewer types of variables to transform. + ###################################################################################### + """ + ### Test Labeler is a very important dictionary that will help transform test data same as train #### + test_labeler = defaultdict(list) + + #### all columns are features except the target column and the folds column ### + if isinstance(target, str): + features = [x for x in list(train) if x not in [target]+idcols] + else: + ### in this case target is a list and hence can be added to idcols + features = [x for x in list(train) if x not in target+idcols] + + ### first find all the types of columns in your data set #### + cats, ints, floats, nlps = EDA_classify_and_return_cols_by_type(train[features]) + + numeric_features = ints + floats + categoricals_features = copy.deepcopy(cats) + nlp_features = copy.deepcopy(nlps) + + test_labeler['categoricals_features'] = categoricals_features + test_labeler['numeric_features'] = numeric_features + test_labeler['nlp_features'] = nlp_features + + return cats, ints, floats, nlps +############################################################################################# +from itertools import combinations +def FE_create_categorical_feature_crosses(dfc, cats): + """ + FE means FEATURE ENGINEERING - That means this function will create new features + ###################################################################################### + This creates feature crosses for each pair of categorical variables in cats. + The number of features created will be n*(n-1)/2 which means 3 cat features will create + 3*2/2 = 3 new features. You must be careful with this function so it doesn't create too many. + + Inputs: + dfc : dataframe containing all the features + cats: a list of categorical features in the dataframe above (dfc) + + Outputs: + dfc: returns the dataframe with newly added features. Original features are untouched. + + ###################################################################################### + Usage: + dfc = FE_create_feature_crosses(dfc, cats) + """ + dfc = copy.deepcopy(dfc) + combos = list(combinations(cats, 2)) + for cat1, cat2 in combos: + dfc.loc[:,cat1+'_cross_'+cat2] = dfc[cat1].astype(str)+" "+dfc[cat2].astype(str) + return dfc +############################################################################################# +from scipy.stats import probplot,skew +def EDA_find_skewed_variables(dft, skew_limit=1.1): + """ + EDA stands for Exploratory Data Analysis : this function performs EDA + ###################################################################################### + This function finds all the highly skewed float (continuous) variables in your DataFrame + It selects them based on the skew_limit you set: anything over skew 1.1 is the default setting. + ###################################################################################### + Inputs: + df: pandas DataFrame + skew_limit: default 1.1 = anything over this limit and it detects it as a highly skewed var. + + Outputs: + list of a variables found that have high skew in data set. + ###################################################################################### + You can use FE_capping_outliers_beyond_IQR_Range() function to cap outliers in these variables. + """ + skewed_vars = [] + conti = dft.select_dtypes(include='float').columns.tolist() + for each_conti in conti: + skew_val=round(dft[each_conti].skew(), 1) + if skew_val >= skew_limit: + skewed_vars.append(each_conti) + print('Found %d skewed variables in data based on skew_limit >= %s' %(len(skewed_vars),skew_limit)) + return skewed_vars +############################################################################################# +def is_outlier(dataframe, thresh=3.5): + if len(dataframe.shape) == 1: + dataframe = dataframe[:,None] + median = np.median(dataframe, axis=0) + diff = np.sum((dataframe - median)**2, axis=-1) + diff = np.sqrt(diff) + med_abs_deviation = np.median(diff) + + modified_z_score = 0.6745 * diff / med_abs_deviation + return modified_z_score > thresh + +def EDA_find_outliers(df, col, thresh=5): + """ + """ + ####### Finds Outliers and marks them as 'True' if they are outliers + ####### Dataframe refers to the input dataframe and threshold refers to how far from the median a value is + ####### I am using the Median Absolute Deviation Method (MADD) to find Outliers here + mask_outliers = is_outlier(df[col],thresh=thresh).astype(int) + return df.iloc[np.where(mask_outliers>0)] +################################################################################### +def outlier_determine_threshold(df, col): + """ + This function automatically determines the right threshold for the dataframe and column. + Threshold is used to determine how many outliers we should detect in the series. + A low threshold will result in too many outliers and a very high threshold will not find any. + This loops until it finds less than 10 times or maximum 1% of data being outliers. + """ + df = df.copy(deep=True) + keep_looping = True + number_of_loops = 1 + thresh = 5 + while keep_looping: + if number_of_loops >= 10: + break + mask_outliers = is_outlier(df[col], thresh=thresh).astype(int) + dfout_index = df.iloc[np.where(mask_outliers>0)].index + pct_outliers = len(dfout_index)/len(df) + if pct_outliers == 0: + if thresh > 5: + thresh = thresh - 5 + elif thresh == 5: + return thresh + else: + thresh = thresh - 1 + elif pct_outliers <= 0.01: + keep_looping = False + else: + thresh_multiplier = int((pct_outliers/0.01)*0.5) + thresh = thresh*thresh_multiplier + number_of_loops += 1 + print(' %s Outlier threshold = %d' %(col, thresh)) + return thresh + +from collections import Counter +def FE_find_and_cap_outliers(df, features, drop=False, verbose=False): + """ + FE at the beginning of function name stands for Feature Engineering. FE functions add or drop features. + ######################################################################################### + Typically we think of outliers as being observations beyond the 1.5 Inter Quartile Range (IQR) + But this function will allow you to cap any observation using MADD method: + MADD: Median Absolute Deviation Method - it's a fast and easy method to find outliers. + In addition, this utility automatically selects the value to cap it at. + -- The value to be capped is based on maximum 1% of data being outliers. + It automatically determines how far away from median the data point needs to be for it to called an outlier. + -- it uses a thresh number: the lower it is, more outliers. It starts at 5 or higher as threshold value. + Notice that it does not use a lower bound to find too low outliers. That you have to do that yourself. + ######################################################################################### + Inputs: + df : pandas DataFrame + features: a single column or a list of columns in your DataFrame + cap_at_nth_largest: default is 5 = you can set it to any integer such as 1, 2, 3, 4, 5, etc. + + Outputs: + df: pandas DataFrame + It returns the same dataframe as you input unless you change drop to True in the input argument. + + Optionally, it can drop certain rows that have too many outliers in at least 3 columns simultaneously. + If drop=True, it will return a smaller number of rows in your dataframe than what you sent in. Be careful! + ######################################################################################### + """ + df = df.copy(deep=True) + outlier_indices = [] + idcol = 'idcol' + df[idcol] = range(len(df)) + if isinstance(features, str): + features = [features] + # iterate over features(columns) + for col in features: + # Determine a list of indices of outliers for feature col + thresh = outlier_determine_threshold(df, col) + mask_outliers = is_outlier(df[col], thresh=thresh).astype(int) + dfout_index = df.iloc[np.where(mask_outliers>0)].index + + df['anomaly1'] = 0 + df.loc[dfout_index ,'anomaly1'] = 1 + + ### this is how the column looks now before capping outliers + if verbose: + fig, (ax1,ax2) = plt.subplots(1,2,figsize=(12,5)) + colors = {0:'blue', 1:'red'} + ax1.scatter(df[idcol], df[col], c=df["anomaly1"].apply(lambda x: colors[x])) + ax1.set_xlabel('Row ID') + ax1.set_ylabel('Target values') + ax1.set_title('%s before capping outliers' %col) + + capped_value = df.loc[dfout_index, col].min() ## this is the value we cap it against + df.loc[dfout_index, col] = capped_value ## maximum values are now capped + ### you are now good to go - you can show how they are capped using before and after pics + if verbose: + colors = {0:'blue', 1:'red'} + ax2.scatter(df[idcol], df[col], c=df["anomaly1"].apply(lambda x: colors[x])) + ax2.set_xlabel('Row ID') + ax2.set_ylabel('Target values') + ax2.set_title('%s after capping outliers' %col) + + # Let's save the list of outliers and see if there are some with outliers in multiple columns + outlier_indices.extend(dfout_index) + + # select certain observations containing more than one outlier in 2 columns or more. We can drop them! + outlier_indices = Counter(outlier_indices) + multiple_outliers = list( k for k, v in outlier_indices.items() if v > 3 ) + ### now drop these rows altogether #### + df = df.drop([idcol,'anomaly1'], axis=1) + if drop: + print('Shape of dataframe before outliers being dropped: %s' %(df.shape,)) + number_of_rows = df.shape[0] + df = df.drop(multiple_outliers, axis=0) + print('Shape of dataframe after outliers being dropped: %s' %(df.shape,)) + print('\nNumber_of_rows with multiple outliers in more than 3 columns which were dropped = %d' %(number_of_rows-df.shape[0])) + return df +################################################################################# +import pandas as pd +import numpy as np +import pdb +from sklearn.utils.validation import check_X_y, check_is_fitted +from sklearn.preprocessing import LabelEncoder +from collections import Counter, defaultdict +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin + +import pandas as pd +import numpy as np +from collections import Counter +import warnings +warnings.filterwarnings("ignore") +################################################################################# +import copy +from sklearn.cluster import KMeans +def FE_kmeans_resampler(x_train, y_train, target, smote="", verbose=0): + """ + This function converts a Regression problem into a Classification problem to enable SMOTE! + This function needs Imbalanced-Learn library. Please pip install it first! + It is a very simple way to send your x_train, y_train in and get back an oversampled x_train, y_train. + Why is this needed in Machine Learning problems? + In Imbalanced datasets, esp. skewed regression problems where the target variable is skewed, this is needed. + Try this on your skewed Regression problems and see what results you get. It should be better. + ---------- + Inputs + ---------- + x_train : pandas dataframe: you must send in the data with predictors only. + min_n_samples : int, default=5: min number of samples below which you combine bins + bins : int, default=3: how many bins you want to split target into + + Outputs + ---------- + n_features_ : int + The number of features of the data passed to :meth:`fit`. + """ + x_train_c = copy.deepcopy(x_train) + x_train_c[target] = y_train.values + + # Regression problem turned into Classification problem + n_clusters = max(3, int(np.log10(len(y_train))) + 1) + # Use KMeans to find natural clusters in your data + km_model = KMeans(n_clusters=n_clusters, + n_init=5, + random_state=99) + #### remember you must predict using only predictor variables! + y_train_c = km_model.fit_predict(x_train) + + if verbose >= 1: + print('Number of clusters created = %d' %n_clusters) + + #### Generate the over-sampled data + #### ADASYN / SMOTE oversampling ##### + if isinstance(smote, str): + x_train_ext, _ = oversample_SMOTE(x_train_c, y_train_c) + else: + x_train_ext, _ = smote.fit_resample(x_train_c, y_train_c) + y_train_ext = x_train_ext[target].values + x_train_ext = x_train_ext.drop(target, axis=1) + return (x_train_ext, y_train_ext) + +################################################################################################### +# Calculate class weight +import copy +from collections import Counter +from sklearn.utils.class_weight import compute_class_weight + +def get_class_distribution(y_input, verbose=0): + y_input = copy.deepcopy(y_input) + if isinstance(y_input, np.ndarray): + class_weights = compute_class_weight('balanced', classes=np.unique(y_input), y=y_input.reshape(-1)) + elif isinstance(y_input, pd.Series): + class_weights = compute_class_weight('balanced', classes=np.unique(y_input.values), y=y_input.values.reshape(-1)) + elif isinstance(y_input, pd.DataFrame): + ### if it is a dataframe, return only if it s one column dataframe ## + y_input = y_input.iloc[:,0] + class_weights = compute_class_weight('balanced', classes=np.unique(y_input.values), y=y_input.values.reshape(-1)) + else: + ### if you cannot detect the type or if it is a multi-column dataframe, ignore it + return None + if len(class_weights[(class_weights> 10)]) > 0: + class_weights = (class_weights/10) + else: + class_weights = (class_weights) + #print(' class_weights = %s' %class_weights) + classes = np.unique(y_input) + xp = Counter(y_input) + class_weights[(class_weights<1)]=1 + class_rows = class_weights*[xp[x] for x in classes] + class_rows = class_rows.astype(int) + min_rows = np.min(class_rows) + class_weighted_rows = dict(zip(classes,class_rows)) + ### sometimes the middle classes are not found in the dictionary ### + for x in range(np.unique(y_input).max()+1): + if x not in list(class_weighted_rows.keys()): + class_weighted_rows.update({x:min_rows}) + else: + pass + ### return the updated dictionary ### + if verbose: + print(' class_weighted_rows = %s' %class_weighted_rows) + return class_weighted_rows + + +def oversample_SMOTE(X,y): + #input DataFrame + #X →Independent Variable in DataFrame\ + #y →dependent Variable in Pandas DataFrame format + # Get the class distriubtion for perfoming relative sampling in the next line + try: + from imblearn.over_sampling import SVMSMOTE + except: + print('This function needs Imbalanced-Learn library. Please pip install it first and try again!') + return + class_weighted_rows = get_class_distribution(y) + smote = SVMSMOTE( random_state=27, + sampling_strategy=class_weighted_rows) + X, y = smote.fit_resample(X, y) + return(X,y) + +def oversample_ADASYN(X,y): + #input DataFrame + #X →Independent Variable in DataFrame\ + #y →dependent Variable in Pandas DataFrame format + # Get the class distriubtion for perfoming relative sampling in the next line + try: + from imblearn.over_sampling import ADASYN + except: + print('This function needs Imbalanced-Learn library. Please pip install it first and try again!') + return + class_weighted_rows = get_class_distribution(y) + # Your favourite oversampler + smote = ADASYN(random_state=27, + sampling_strategy=class_weighted_rows) + X, y = smote.fit_resample(X, y) + return(X,y) +############################################################################# +import numpy as np +import pandas as pd +from sklearn.model_selection import train_test_split +def split_data_n_ways(df, target, n_splits, test_size=0.2, modeltype=None,**kwargs): + """ + Inputs: + df: dataframe that you want to split + target: the target variable in data frame (df) + n_splits: number of ways in which you want to split the data frame (default=3) + test_size: size of the test dataset: default is 0.2 But it splits this test into valid and test half. + Hence you will get 10% of df as test and 10% of df as valid and remaining 80% as train + ################ how it works ################################################ + You can split a dataframe three ways or six ways depending on your need. Three ways is: + train, valid, test + Six ways can be: + X_train,y_train, X_valid, y_valid, X_test, y_test + You will get a list containing these dataframes...depending on what you entered as number of splits + Output: List of dataframes + """ + if kwargs: + for key, val in kwargs: + if key == 'modeltype': + key = val + if key == 'test_size': + test_size = val + if modeltype is None: + if isinstance(target, str): + if df[target].dtype == float: + modeltype = 'Regression' + else: + modeltype = 'Classification' + target = [target] + else: + if df[target[0]].dtype == float: + modeltype = 'Regression' + else: + modeltype = 'Classification' + preds = [x for x in list(df) if x not in target] + print('Number of predictors in dataset: %d' %len(preds)) + list_of_dfs = [] + if modeltype == 'Regression': + nums = int((1-test_size)*df.shape[0]) + train, testlarge = df[:nums], df[nums:] + else: + train, testlarge = train_test_split(df, test_size=test_size, random_state=42) + list_of_dfs.append(train) + if n_splits == 2: + print('Returning a Tuple with two dataframes and shapes: (%s,%s)' %(train.shape, testlarge.shape)) + return train, testlarge + elif modeltype == 'Regression' and n_splits == 3: + nums2 = int(0.5*(testlarge.shape[0])) + valid, test = testlarge[:nums2], testlarge[nums2:] + print('Returning a Tuple with three dataframes and shapes: (%s,%s,%s)' %(train.shape, valid.shape, test.shape)) + return train, valid, test + elif modeltype == 'Classification' and n_splits == 3: + valid, test = train_test_split(testlarge, test_size=0.5, random_state=99) + print('Returning a Tuple with three dataframes and shapes: (%s,%s,%s)' %(train.shape, valid.shape, test.shape)) + return train, valid, test + #### Continue only if you need more than 3 splits ###### + if modeltype == 'Regression': + nums2 = int(0.5*(df.shape[0] - nums)) + valid, test = testlarge[:nums2], testlarge[nums2:] + if n_splits == 4: + X_train, y_train, X_test, y_test = train[preds], train[target], testlarge[preds], testlarge[target] + list_of_dfs = [X_train,y_train, X_test, y_test] + print('Returning a Tuple with 4 dataframes: (%s %s %s %s)' %(X_train.shape,y_train.shape, + X_test.shape,y_test.shape)) + return list_of_dfs + elif n_splits == 6: + X_train, y_train, X_valid, y_valid, X_test, y_test = train[preds], train[target], valid[ + preds], valid[target], test[preds], test[target] + list_of_dfs = [X_train,y_train, X_valid, y_valid, X_test, y_test] + print('Returning a Tuple with six dataframes and shapes: (%s %s %s %s,%s,%s)' %( + X_train.shape,y_train.shape, X_valid.shape,y_valid.shape,X_test.shape,y_test.shape)) + return list_of_dfs + else: + print('Number of splits must be 2, 3, 4 or 6') + return + else: + if n_splits == 4: + X_train, y_train, X_test, y_test = train[preds], train[target], testlarge[preds], testlarge[target] + list_of_dfs = [X_train,y_train, X_test, y_test] + print('Returning a Tuple with 4 dataframes: (%s %s %s %s)' %(X_train.shape,y_train.shape, + X_test.shape,y_test.shape)) + return list_of_dfs + elif n_splits == 6: + X_train, y_train, X_valid, y_valid, X_test, y_test = train[preds], train[target], valid[ + preds], valid[target], test[preds], test[target] + print('Returning 4 dataframes:', X_train.shape, y_train.shape, X_test.shape, y_test.shape) + list_of_dfs = [X_train,y_train, X_valid, y_valid, X_test, y_test] + print('Returning a Tuple with six dataframes and shapes: (%s %s %s %s,%s,%s)' %( + X_train.shape,y_train.shape, X_valid.shape,y_valid.shape,X_test.shape,y_test.shape)) + return list_of_dfs + else: + print('Number of splits must be 2, 3, 4 or 6') + return +################################################################################## +def FE_concatenate_multiple_columns(df, cols, filler=" ", drop=True): + """ + This handy function combines multiple string columns into a single NLP text column. + You can do further pre-processing on such a combined column with TFIDF or BERT style embedding. + + Inputs + --------- + df: pandas dataframe + cols: string columns that you want to concatenate into a single combined column + filler: string (default: " "): you can input any string that you want to combine them with. + drop: default True. If True, drop the columns input. If False, keep the columns. + + Outputs: + ---------- + df: there will be a new column called ['combined'] that will be added to your dataframe. + """ + df = df.copy(deep=True) + df['combined'] = df[cols].apply(lambda row: filler.join(row.values.astype(str)), axis=1) + if drop: + df = df.drop(cols, axis=1) + return df +################################################################################## +from sklearn.preprocessing import KBinsDiscretizer +from sklearn.mixture import GaussianMixture + +def FE_discretize_numeric_variables(train, bin_dict, test='', strategy='kmeans',verbose=0): + """ + This handy function discretizes numeric variables into binned variables using kmeans algorithm. + You need to provide the names of the variables and the numbers of bins for each variable in a dictionary. + It will return the same dataframe with new binned variables that it has created. + + Inputs: + ---------- + df : pandas dataframe - please ensure it is a dataframe. No arrays please. + bin_dict: dictionary of names of variables and the bins that you want for each variable. + strategy: default is 'kmeans': but you can choose: {'gauusian','uniform', 'quantile', 'kmeans'} + + Outputs: + ---------- + df: pandas dataframe with new variables with names such as: variable+'_discrete' + """ + df = copy.deepcopy(train) + test = copy.deepcopy(test) + num_cols = len(bin_dict) + nrows = int((num_cols/2)+0.5) + #print('nrows',nrows) + if verbose: + fig = plt.figure(figsize=(10,3*num_cols)) + for i, (col, binvalue) in enumerate(bin_dict.items()): + new_col = col+'_discrete' + if strategy == 'gaussian': + kbd = GaussianMixture(n_components=binvalue, random_state=99) + df[new_col] = kbd.fit_predict(df[[col]]).astype(int) + if not isinstance(test, str): + test[new_col] = kbd.predict(test[[col]]).astype(int) + else: + kbd = KBinsDiscretizer(n_bins=binvalue, encode='ordinal', strategy=strategy) + df[new_col] = kbd.fit_transform(df[[col]]).astype(int) + if not isinstance(test, str): + test[new_col] = kbd.transform(test[[col]]).astype(int) + if verbose: + ax1 = plt.subplot(nrows,2,i+1) + ax1.scatter(df[col],df[new_col]) + ax1.set_title(new_col) + if not isinstance(test, str): + return df, test + else: + return df +################################################################################## +def FE_transform_numeric_columns_to_bins(df, bin_dict, verbose=0): + """ + This handy function discretizes numeric variables into binned variables using kmeans algorithm. + You need to provide the names of the variables and the numbers of bins for each variable in a dictionary. + It will return the same dataframe with new binned variables that it has created. + + Inputs: + ---------- + df : pandas dataframe - please ensure it is a dataframe. No arrays please. + bin_dict: dictionary of names of variables and the kind of transformation you want + default is 'log': but you can choose: {'log','log10', 'sqrt', 'max-abs'} + + Outputs: + ---------- + df: pandas dataframe with new variables with names such as: variable+'_discrete' + """ + df = copy.deepcopy(df) + num_cols = len(bin_dict) + nrows = int((num_cols/2)+0.5) + if verbose: + fig = plt.figure(figsize=(10,3*num_cols)) + for i, (col, binvalue) in enumerate(bin_dict.items()): + new_col = col+'_'+binvalue + if binvalue == 'log': + print('Warning: Negative values in %s have been made positive before log transform!' %col) + df.loc[df[col]==0,col] = 1e-15 ### make it a small number + df[new_col] = np.abs(df[col].values) + df[new_col] = np.log(df[new_col]).values + elif binvalue == 'log10': + print('Warning: Negative values in %s have been made positive before log10 transform!' %col) + df.loc[df[col]==0,col] = 1e-15 ### make it a small number + df[new_col] = np.abs(df[col].values) + df[new_col] = np.log10(df[new_col]).values + elif binvalue == 'sqrt': + print('Warning: Negative values in %s have been made positive before sqrt transform!' %col) + df[new_col] = np.abs(df[col].values) ### make it a small number + df[new_col] = np.sqrt(df[new_col]).values + elif binvalue == 'max-abs': + print('Warning: Negative values in %s have been made positive before max-abs transform!' %col) + col_max = max(np.abs(df[col].values)) + if col_max == 0: + col_max = 1 + df[new_col] = np.abs(df[col].values)/col_max + else: + print('Warning: Negative values in %s have been made positive before log transform!' %col) + df.loc[df[col]==0,col] = 1e-15 ### make it a small number + df[new_col] = np.abs(df[col].values) + df[new_col] = np.log(df[new_col]).values + if verbose: + ax1 = plt.subplot(nrows,2,i+1) + df[col].plot.kde(ax=ax1, label=col,alpha=0.5,color='r') + ax2 = ax1.twiny() + df[new_col].plot.kde(ax=ax2,label=new_col,alpha=0.5,color='b') + plt.legend(); + return df +################################################################################# +from itertools import cycle, combinations +def FE_create_interaction_vars(df, intxn_vars): + """ + This handy function creates interaction variables among pairs of numeric vars you send in. + Your input must be a dataframe and a list of tuples. Each tuple must contain a pair of variables. + All variables must be numeric. Double check your input before sending them in. + """ + if type(df) == dask.dataframe.core.DataFrame: + ## skip if it is a dask dataframe #### + pass + else: + df = df.copy(deep=True) + combos = combinations(intxn_vars, 2) + ### I have tested this for both category and object dtypes so don't worry ### + for (each_intxn1,each_intxn2) in combos: + new_col = each_intxn1 + '_x_' + each_intxn2 + try: + df[new_col] = df[each_intxn1].astype(str) + ' ' + df[each_intxn2].astype(str) + except: + continue + ### this will return extra features generated by interactions #### + return df +################################################################################ +def FE_create_interaction_vars_train(df, intxn_vars): + """ + This handy function creates interaction variables among pairs of numeric vars you send in. + Your input must be a dataframe and a list of tuples. Each tuple must contain a pair of variables. + All variables must be numeric. Double check your input before sending them in. + """ + if type(df) == dask.dataframe.core.DataFrame: + ## skip if it is a dask dataframe #### + pass + else: + df = df.copy(deep=True) + combos = combinations(intxn_vars, 2) + ### This is only for integer vars ### + for (each_intxn1,each_intxn2) in combos: + new_col = each_intxn1 + '_x_' + each_intxn2 + try: + df[new_col] = df[each_intxn1].astype(str) + ' ' + df[each_intxn2].astype(str) + except: + continue + ### this will return extra features generated by interactions #### + return df +################################################################################### +def FE_create_interaction_vars_test(df, intxn_vars, combo_vars): + """ + This handy function creates interaction variables among pairs of numeric vars you send in. + Your input must be a dataframe and a list of tuples. Each tuple must contain a pair of variables. + All variables must be numeric. Double check your input before sending them in. + """ + if type(df) == dask.dataframe.core.DataFrame: + ## skip if it is a dask dataframe #### + pass + else: + df = df.copy(deep=True) + combos = combinations(intxn_vars, 2) + ### I have tested this for both category and object dtypes so don't worry ### + newcols = [] + copy_combos = copy.deepcopy(combos) + for (each_intxn1,each_intxn2) in copy_combos: + new_col = each_intxn1 + '_x_' + each_intxn2 + newcols.append(new_col) + left_vars = left_subtract(newcols, combo_vars) + + if len(left_vars) > 0: + for each_left in left_vars: + df[each_left] = 0 + for (each_intxn1,each_intxn2) in combos: + new_col = each_intxn1 + '_x_' + each_intxn2 + try: + df[new_col] = df[each_intxn1].astype(str) + ' ' + df[each_intxn2].astype(str) + except: + continue + ### this will return extra features generated by interactions #### + return df +################################################################################## +import matplotlib.pyplot as plt +def EDA_binning_numeric_column_displaying_bins(dft, target, bins=4, test=""): + """ + This splits the data column into the number of bins specified and returns labels, bins, and dataframe. + Outputs: + labels = the names of the bins + edges = the edges of the bins + dft = the dataframe with an added column called "binned_"+name of the column you sent in + """ + dft = copy.deepcopy(dft) + _, edges = pd.qcut(dft[target].dropna(axis=0),q=bins, retbins=True, duplicates='drop') + ### now we create artificial labels to match the bins edges #### + ls = [] + for i, x in enumerate(edges): + #print('i = %s, next i = %s' %(i,i+1)) + if i < len(edges)-1: + ls.append('from_'+str(round(edges[i],3))+'_to_'+str(round(edges[i+1],3))) + ##### Next we add a column to hold the bins created by above ############### + dft['binned_'+target] = pd.cut(dft[target], bins=edges, retbins=False, labels=ls, include_lowest=True).values.tolist() + if not isinstance(test, str): + test['binned_'+target] = pd.cut(test[target], bins=edges, retbins=False, labels=ls, include_lowest=True).values.tolist() + nrows = int(len(edges)/2 + 1) + plt.figure(figsize=(15,nrows*3)) + plt.subplots_adjust(hspace=.5) + collect_bins = [] + for i in range(len(edges)): + if i == 0: + continue + else: + dftc = dft[(dft[target]>edges[i-1]) & (dft[target]<=edges[i])] + collect_bins.append(dftc) + ax1 = plt.subplot(nrows, 2, i) + dftc[target].hist(bins=30, ax=ax1) + ax1.set_title('bin %d: size: %d, %s %0.2f to %0.2f' %(i, dftc.shape[0], target, + edges[i-1], edges[i])) + return ls, edges, dft, test +######################################################################################### +from pathlib import Path +import matplotlib.pyplot as plt +import seaborn as sns +from datetime import datetime, date +from sklearn.metrics import mean_squared_error, roc_auc_score +from sklearn.preprocessing import minmax_scale +#### This is where we add other libraries to form a pipeline ### +import copy +import time +import re +from scipy.ndimage import convolve +from sklearn import datasets, metrics +from sklearn.model_selection import train_test_split +from sklearn.pipeline import Pipeline +from sklearn.model_selection import KFold, StratifiedShuffleSplit + +def add_text_paddings(train_data,nlp_column,glove_filename_with_path,tokenized, + fit_flag=True, + max_length=100): + """ + ################################################################################################## + This function uses a GloVe pre-trained model to add embeddings to your data set. + ######## I N P U T ##############################: + data: DataFrame + nlp_column: name of the NLP column in the DataFrame + target: name of the target variable in the DataFrame + glovefile: location of where the glove.txt file is. You must give the full path to that file. + max_length: specify the dimension of the glove vector you can have upto the dimension of the glove txt file. + Make sure you don't exceed the dimension specified in the glove.txt file. Otherwise, error result. + ####### O U T P U T ############################# + The dataframe is split into train and test and are modified into the specified vector dimension of max_length + X_train_padded: the train dataframe with dimension specified in max_length + y_train: the target vector using data and target column + X_test_padded: the test dataframe with dimension specified in max_length + tokenized: This is the tokenizer that was used to split the words in data set. This must be used later. + ################################################################################################## + """ + train_index = train_data.index + ### Encode Train data text into sequences + train_data_encoded = tokenized.texts_to_sequences(train_data[nlp_column]) + ### Pad_Sequences function is used to make lists of unequal length to stacked sets of padded and truncated arrays + ### Pad Sequences for Train + X_train_padded = pad_sequences(train_data_encoded, + maxlen=max_length, + padding='post', + truncating='post') + print(' Data shape after padding = %s' %(X_train_padded.shape,)) + new_cols = ['glove_dim_' + str(x+1) for x in range(X_train_padded.shape[1])] + X_train_padded = pd.DataFrame(X_train_padded, columns=new_cols, index=train_index) + if fit_flag: + return X_train_padded, tokenized, vocab_size + else: + return X_train_padded +##################################################################################################### +def load_embeddings(tokenized,glove_filename_with_path,vocab_size,glove_dimension): + """ + ################################################################################################## + # glove_filename_with_path: Make sure u have downloaded and unzipped the GloVe ".txt" file to the location here. + # we now create a dictionary that maps GloVe tokens to 100, or 200- or 300-dimensional real-valued vectors + # Then we load the whole embedding into memory. Make sure you have plenty of memory in your machine! + ################################################################################################## + """ + MAX_NUM_WORDS = 100000 + glove_path = Path(glove_filename_with_path) + print(' Creating embeddings. This will take time...') + embeddings_index = dict() + for line in glove_path.open(encoding='latin1'): + values = line.split() + word = values[0] + try: + coefs = np.asarray(values[1:], dtype='float32') + except: + continue + embeddings_index[word] = coefs + print('Loaded {:,d} Glove vectors.'.format(len(embeddings_index))) + #There are around 340,000 word vectors that we use to create an embedding matrix + # that matches the vocabulary so that the RNN model can access embeddings by the token index + # prepare embedding matrix + word_index = tokenized.word_index + embedding_matrix = np.zeros((vocab_size, glove_dimension)) + print('Preparing embedding matrix.') + for word, i in word_index.items(): + if i >= MAX_NUM_WORDS: + continue + embedding_vector = embeddings_index.get(word) + if embedding_vector is not None: + # words not found in embedding index will be all-zeros. + embedding_matrix[i] = embedding_vector + print(' Completed.') + return embedding_matrix, glove_dimension +##################################################################################################### +import copy +def FE_convert_mixed_datatypes_to_string(df): + df = copy.deepcopy(df) + for col in df.columns: + if len(df[col].apply(type).value_counts()) > 1: + print('Mixed data type detected in %s column. Converting all rows to string type now...' %col) + df[col] = df[col].map(lambda x: x if isinstance(x, str) else str(x)).values + if len(df[col].apply(type).value_counts()) == 1: + print(' completed.') + else: + print(' could not change column type. Fix it manually and then re-run EDA.') + return df +################################################################################## +def remove_duplicate_cols_in_dataset(df): + df = copy.deepcopy(df) + number_duplicates = df.columns.duplicated().astype(int).sum() + duplicates = df.columns[df.columns.duplicated()] + if number_duplicates > 0: + print('Removing %d duplicate column(s) of %s' %(number_duplicates, duplicates)) + df = df.loc[:,~df.columns.duplicated()] + return df +########################################################################### +def FE_split_list_into_columns(df, col, cols_in=[]): + """ + This is a Feature Engineering function. It will automatically detect object variables that contain lists + and convert them into new columns. You need to provide the dataframe, the name of the object column. + Optionally, you can decide to send the names of the new columns you want to create as cols_in. + It will return the dataframe with additional columns. It will drop the column which you sent in as input. + + Inputs: + -------- + df: pandas dataframe + col: name of the object column that contains a list. Remember it must be a list and not a string. + cols_in: names of the columns you want to create. If the number of columns is less than list length, + it will automatically choose only the fist few items of the list to match the length of cols_in. + + Outputs: + --------- + df: pandas dataframe with new columns and without the column you sent in as input. + """ + df = copy.deepcopy(df) + if cols_in: + max_col_length = len(cols_in) + df[cols_in] = df[col].apply(pd.Series).values[:,:max_col_length] + df = df.drop(col,axis=1) + else: + if len(df[col].map(type).value_counts())==1 and df[col].map(type).value_counts().index[0]==list: + ### Remember that fillna only works at dataframe level! ### + max_col_length = df[[col]].fillna('missing').map(len).max() + cols = [col+'_'+str(i) for i in range(max_col_length)] + df[cols] = df[col].apply(pd.Series) + df = df.drop(col,axis=1) + else: + print('Column %s does not contain lists or has mixed types other than lists. Fix it and rerun.' %col) + return df +############################################################################################# +def EDA_randomly_select_rows_from_dataframe(train_dataframe, targets, nrows_limit, DS_LEN=''): + maxrows = 10000 + train_dataframe = copy.deepcopy(train_dataframe) + copy_targets = copy.deepcopy(targets) + if not DS_LEN: + DS_LEN = train_dataframe.shape[0] + ####### we randomly sample a small dataset to classify features ##################### + test_size = min(0.9, (1 - (maxrows/DS_LEN))) ### make sure there is a small train size + if test_size <= 0: + test_size = 0.9 + ### Float variables are considered Regression ##################################### + modeltype, _ = analyze_problem_type(train_dataframe[copy_targets], copy_targets, verbose=0) + ####### If it is a classification problem, you need to stratify and select sample ### + if modeltype != 'Regression': + print(' loading a random sample of %d rows into pandas for EDA' %nrows_limit) + for each_target in copy_targets: + ### You need to remove rows that have very class samples - that is a problem while splitting train_small + list_of_few_classes = train_dataframe[each_target].value_counts()[train_dataframe[each_target].value_counts()<=3].index.tolist() + train_dataframe = train_dataframe.loc[~(train_dataframe[each_target].isin(list_of_few_classes))] + try: + train_small, _ = train_test_split(train_dataframe, test_size=test_size, stratify=train_dataframe[targets]) + except: + ## This split sometimes errors. It is then better to split using a random sample ## + train_small = train_dataframe.sample(n=nrows_limit, replace=True, random_state=99) + else: + ### For Regression problems: load a small sample of data into a pandas dataframe ## + print(' loading a sequential sample of %d rows into pandas for EDA' %nrows_limit) + train_small = train_dataframe[:nrows_limit] + return train_small +################################################################################################ +from lazytransform import LazyTransformer +import pdb +class FeatureWiz(BaseEstimator, TransformerMixin): + """ + FeatureWiz is a feature selection and engineering tool compatible with scikit-learn. + It automates the process of selecting the most relevant features for a dataset and + supports various encoding, scaling, and data preprocessing techniques. + + Parameters + ---------- + corr_limit : float, default=0.90 + The correlation limit to consider for feature selection. Features with correlations + above this limit may be excluded. + + verbose : int, default=0 + Level of verbosity in output messages. + + feature_engg : str or list, default=''. List = ['interactions', 'groupby', 'target', + 'dae', 'vae', 'dae_add', 'vae_add'] + + It specifies the feature engineering methods to apply, such as 'interactions', 'groupby', + and 'target'. Two new feature engg types have been added: + 1. First is called "dae" (or dae_add) which will call a Denoising Auto Encoder to create + a low-dimensional representation of the original data by reconstructing the + original data from noisy types for a multi-class problem. Use this selectively + for multi-class or highly imbalanced datasets to improve Classifier + performance. 'dae' will replace X while 'dae_add' will add features to X. + 2. The second is called "vae" (or vae_add) which stands for Variational Autoencoder (VAE) + which can be very useful for multi-class problemns. VAE is a type of generative model + that not only learns a compressed representation of the data (like a traditional autoencoder) + but also models the underlying probability distribution of the data. + This can be particularly useful in multi-class problems, especially when + dealing with complex datasets or when you need to generate new samples for + data augmentation. 'vae' will replace X while 'vae_add' will add features to X. + + ae_options : must be a dict, default={}. Possible values for auto encoders can be sent + via this dictionary such as the following examples. You can even use it to GridSearch. + For 'dae', use this dict: {'encoding_dim': 50, 'noise_factor': 0.1, 'learning_rate': 0.001, + 'epochs': 100, 'batch_size': 16, + 'callbacks':keras.callbacks.EarlyStopping(monitor="val_loss", min_delta=0.001, patience=10), + 'use_simple_architecture': None} + For 'vae', use this dict: {'intermediate_dim':64, 'latent_dim': 4, 'epochs': 50, + 'batch_size': 16, 'learning_rate': 0.01} + For 'gan', use this dict: {'input_dim':10, "embedding_dim': 100, 'epochs': 50, + 'num_synthetic_samples': } + + + category_encoders : str or list, default='' + Encoders for handling categorical variables. Supported encoders include 'onehot', + 'ordinal', 'hashing', 'count', 'catboost', 'target', 'glm', 'sum', 'woe', 'bdc', + 'loo', 'base', 'james', 'helmert', 'label', 'auto', etc. + + add_missing : bool, default=False + If True, adds indicators for missing values in the dataset. + + dask_xgboost_flag : bool, default=False + If set to True, enables the use of Dask for parallel computing with XGBoost. + + nrows : int or None, default=None + Limits the number of rows to process. + + skip_sulov : bool, default=False + If True, skips the application of the Super Learning Optimized (SULO) method in + feature selection. + + skip_xgboost : bool, default=False + If True, bypasses the recursive XGBoost feature selection. + + transform_target : bool, default=False + When True, transforms the target variable(s) into numeric format if they are not + already. + + scalers : str or None, default=None + Specifies the scaler to use for feature scaling. Available options include + 'std', 'standard', 'minmax', 'max', 'robust', 'maxabs'. + + Attributes + ---------- + features : list + List of selected features after feature selection process. + + Examples + -------- + >>> wiz = FeatureWiz(feature_engg = '', nrows=None, transform_target=True, scalers="std", + category_encoders="auto", add_missing=False, verbose=0) + >>> X_train_selected, y_train = wiz.fit_transform(X_train, y_train) + >>> X_test_selected = wiz.transform(X_test) + >>> selected_features = wiz.features + + Notes + ----- + - The class is built to automatically determine the most suitable encoder and scaler + based on the dataset characteristics, unless explicitly specified by the user. + - FeatureWiz is designed to work with both numerical and categorical variables, + applying various preprocessing steps such as missing value flagging, feature + engineering, and feature selection. + - It's important to note that using extensive feature engineering can lead to a + significant increase in the number of features, which may affect processing time. + + Raises + ------ + ValueError + If inputs are not in the expected format or if invalid parameters are provided. + """ + + def __init__(self, corr_limit=0.90, verbose=0, feature_engg='', category_encoders='', + add_missing=False, dask_xgboost_flag=False, nrows=None, + skip_sulov=False, skip_xgboost=False, transform_target=False, + scalers=None, imbalanced=False, ae_options={}): + """ + Initialize the FeatureWiz class with the given parameters. + """ + self.features = None + self.corr_limit = corr_limit + self.verbose = verbose + self.add_missing = add_missing + self.feature_engg = self._parse_feature_engg(feature_engg) + self.category_encoders = self._parse_category_encoders(category_encoders) + #print(' %s parsed as encoders...' %self.category_encoders) + self.dask_xgboost_flag = dask_xgboost_flag + self.nrows = nrows + self.skip_sulov = skip_sulov + self.skip_xgboost = skip_xgboost + self.transform_target = transform_target + if scalers is None: + self.scalers = '' + elif isinstance(scalers, str): + self.scalers = scalers.lower() + self.imbalanced=imbalanced + self.ae_options = ae_options + self._initialize_other_attributes() + + def _parse_feature_engg(self, feature_engg): + #### This is complicated logic ### be careful changing it! + if isinstance(feature_engg, str): + if feature_engg == '': + return [] + else: + return [feature_engg] + elif feature_engg is None: + return [] + elif isinstance(feature_engg, list): + return feature_engg + else: + print('feature engg must be a list of strings or a string') + return [] + + def _parse_category_encoders(self, encoders): + approved_encoders = { + 'onehot', 'ordinal', 'hashing', 'count', 'catboost', + 'target', 'glm', 'sum', 'woe', 'bdc', 'loo', 'base', + 'james', 'helmert', 'label', 'auto' + } + #### This is complicated logic ### be careful changing it! + if isinstance(encoders, str): + if encoders == '': + encoders = 'auto' + else: + ### first create a list and then check for validity of each one ### + encoders = [encoders] + encoders = [e for e in encoders if e in approved_encoders] + #### Leave the next line as "if" - Don't change it to "elif" + if isinstance(encoders, list): + if encoders == []: + encoders = 'auto' + ###### let us find approved encoders, if not send 'auto' ### + return encoders + + def _initialize_other_attributes(self): + self.model_type = '' + self.grouper = None + self.targeter = None + self.numvars = [] + self.catvars = [] + self.missing_flags = [] + self.cols_zero_variance = [] + self.target = None + self.targets = None + ### setting autoencoder to None ### + self.ae = None + encoders_dict = { + 'OneHotEncoder': 'onehot', + 'OrdinalEncoder': 'ordinal', + 'HashingEncoder': 'hashing', + 'CountEncoder': 'count', + 'CatBoostEncoder': 'catboost', + 'TargetEncoder': 'target', + 'GLMMEncoder': 'glm', + 'SumEncoder': 'sum', + 'WOEEncoder': 'woe', + 'BackwardDifferenceEncoder': 'bdc', + 'LeaveOneOutEncoder': 'loo', + 'BaseNEncoder': 'base', + 'JamesSteinEncoder': 'james', + 'HelmertEncoder': 'helmert', + 'label': 'label', + 'auto': 'auto', + } + approved_encoders = ['onehot','ordinal', 'hashing','count','catboost', + 'target','glm','sum','woe','bdc','loo','base', + 'james','helmert', 'label','auto'] + encoders = [] + for each_encoder in self.category_encoders: + if not each_encoder in approved_encoders: + enc = encoders_dict.get(each_encoder, 'label') + encoders.append(enc) + else: + ### if they are in approved list check if they chose auto! + if each_encoder == 'auto': + encoders = ['onehot', 'label'] + else: + encoders.append(each_encoder) + print('featurewiz is given %0.1f as correlation limit...' %self.corr_limit) + if len(encoders) > 2: + encoders = encoders[:2] + #print(' %s given as encoders...' %encoders) + #### This is complicated logic. Be careful before changing it! + self.category_encoders = encoders + feature_generators = ['interactions', 'groupby', 'target', 'dae', 'vae', 'dae_add', + 'vae_add', 'gan'] + feature_gen = [] + if self.feature_engg: + print(' Warning: Too many features will be generated since feature engg specified') + if isinstance(self.feature_engg, str): + self.feature_engg = [self.feature_engg] + #### Once you have made it into a list, now do all this processing. + ### Don't change the next line to elif. It needs to be if! I know! + if isinstance(self.feature_engg, list): + for each_fe in self.feature_engg: + if each_fe in feature_generators: + if each_fe == 'target': + if self.category_encoders == 'auto': + ### Convert it to two encoders from one since they added target encoding + self.category_encoders = ['label'] + self.category_encoders.append(each_fe) + else: + ### otherwise just add 'target' to the existing list of cat encoders + self.category_encoders.append(each_fe) + print(' moving target encoder from feature_engg to category_encoders list') + else: + feature_gen.append(each_fe) + else: + print('feature engg types must be one or more of: %s. Continuing...' %feature_generators) + self.feature_engg = [] + feature_gen = [] + self.feature_gen = copy.deepcopy(feature_gen) + print(' final list of feature engineering given: %s' %self.feature_gen) + else: + print(' Skipping feature engineering since no feature_engg input...') + self.feature_gen = [] + #### This is complicated logic. Be careful before changing it! + if len(self.category_encoders) == 1: + self.category_encoders = ['label'] + self.category_encoders + if self.category_encoders: + print(' final list of category encoders given: %s' %self.category_encoders) + + ### all Auto Encoders need their features to be scaled - MinMax works best! + try: + if 'dae' in self.feature_gen or 'dae_add' in self.feature_gen: + self.ae = DenoisingAutoEncoder(**self.ae_options) + ### If user does not give input on scalers, then use one of your own + if self.scalers is None: + self.scalers='minmax' + elif 'vae' in self.feature_gen or 'vae_add' in self.feature_gen: + self.ae = VariationalAutoEncoder(**self.ae_options) + ### If user does not give input on scalers, then use one of your own + if self.scalers is None: + self.scalers='minmax' + elif 'gan' in self.feature_gen: + self.ae = GANAugmenter(**self.ae_options) + #self.ae = GANAugmenter(gan_model=None, input_dim=None, + # embedding_dim=100, epochs=10, + # num_synthetic_samples=1000) + ### If user does not give input on scalers, then use one of your own + if self.scalers is None: + self.scalers='minmax' + ### print the options for Auto Encoder if available ## + if self.ae: + print('AutoEncoder %s\n AE options: %s' %(self.ae, + self.ae_options.items())) + except Exception as e: + print('ae_options erroring due to %s. Please check documentation and try again.' %e) + + print(' final list of scalers given: [%s]' %self.scalers) + #### Now you can set up the parameters for Lazy Transformer + self.lazy = LazyTransformer(model=None, encoders=self.category_encoders, + scalers=self.scalers, date_to_string=False, + transform_target=self.transform_target, imbalanced=self.imbalanced, + save=False, combine_rare=False, verbose=self.verbose) + + def fit(self, X, y): + max_cats = 10 + if isinstance(X, np.ndarray): + print('X input must be a dataframe since we use column names to build data pipelines. Returning') + return X, y + if isinstance(y, np.ndarray): + print(' y input is an numpy array and hence convert into a series or dataframe and re-try.') + return X, y + ### Now you can process the X and y datasets #### + if isinstance(y, pd.Series): + self.target = y.name + self.targets = [self.target] + if self.target is None: + print(' y input is a pandas series with no name. Convert it and re-try.') + return X, y + elif isinstance(y, pd.DataFrame): + self.target = y.columns.tolist() + self.targets = y.columns.tolist() + elif isinstance(X, np.ndarray): + print('y must be a pd.Series or pd.DataFrame since we use column names to build data pipeline. Returning') + return {}, {} + ###################################################################################### + ##### MAKING FEATURE_TYPE AND FEATURE_GEN SELECTIONS HERE ############# + ###################################################################################### + X_sel = copy.deepcopy(X) + print('Loaded input data. Shape = %s' %(X_sel.shape,)) + ##### This where we find the features to modify ###################### + preds = [x for x in list(X_sel) if x not in self.targets] + ### This is where we sort the columns to make sure that the order of columns doesn't matter in selection ########### + numvars = X_sel[preds].select_dtypes(include = 'number').columns.tolist() + self.numvars = numvars + if self.verbose: + print(' selecting %d numeric features for further processing...' %len(numvars)) + catvars = left_subtract(preds, numvars) + self.catvars = catvars + if len(self.catvars) > max_cats: + print(' Warning: Too many features will be generated since categorical vars > %s. This may take time...' %max_cats) + return self + + def fit_transform(self, X, y): + self.fit(X, y) + X_sel, y_sel = self.transform(X, y) + return X_sel, y_sel + + def transform(self, X, y=None): + start_time = time.time() + + if y is None: + ########################################################## + ############# This is only for test data ################# + ########################################################## + ### Now you can process the X dataset #### + print('#### Starting featurewiz transform for test data ####') + if isinstance(X, np.ndarray): + print('X must be a pd.Series or pd.DataFrame since we use column names to build data pipeline. Returning') + return {} + print('Loaded input data. Shape = %s' %(X.shape,)) + if self.add_missing: + print(' Caution: add_missing adds a missing flag column for every column in your dataset. Beware...') + X = add_missing(X) + print(' transformed dataset shape: %s' %(X.shape,)) + if self.feature_gen: + print(' Beware! feature_engg will add 100s, if not 1000s of additional features to your dataset!') + if np.where('groupby' in self.feature_gen,True, False).tolist(): + if not self.grouper is None: + X = self.grouper.transform(X) + + if np.where('interactions' in self.feature_gen,True, False).tolist(): + X = FE_create_interaction_vars_train(X, self.catvars) + + ### this is only for test data ###### + print('#### Starting lazytransform for test data ####') + X_sel = self.lazy.transform(X) + + ### Sometimes the index becomes huge after imabalanced flag is set! + X_index = X_sel.index + #### This is where you transform using the Denoising Auto Encoder + if not self.ae is None: + #### It is okay if y is None ######## + if not 'gan' in self.feature_gen: + X_sel_ae = self.ae.transform(X_sel) + else: + ### if it is GAN just return the dataframe as it is + return X_sel[self.features] + + if np.all(np.isnan(X_sel_ae)): + print('Auto encoder is erroring. Using existing features shape: %s' %(X_sel.shape,)) + else: + ### Since this results in a higher dimension you need to create new columns ## + new_vars = ['feature_'+str(x) for x in range(X_sel_ae.shape[1])] + X_sel_ae = pd.DataFrame(X_sel_ae, columns=new_vars, index=X_index) + if 'dae_add' in self.feature_gen or 'vae_add' in self.feature_gen: + ### Only if add is specified do you add the features to X + X_sel = pd.concat([X_sel, X_sel_ae], axis=1) + else: + ### Just replace X_sel with X_sel_ae ### + X_sel = copy.deepcopy(X_sel_ae) + print('Shape of transformed data due to auto encoder = %s' %(X_sel.shape,)) + + ### return either fitted features or all features depending on error ### + if len(self.cols_zero_variance) > 0: + print(' Dropping %d columns due to zero variance...' %len(self.cols_zero_variance)) + X_sel = X_sel.drop(self.cols_zero_variance, axis=1) + print('Returning dataframe with %d features ' %len(self.features)) + + try: + return X_sel[self.features] + except: + print('Returning dataframe with all features since error in feature selection...') + return X_sel + else: + ########################################################## + ###################### this is only for train data ####### + ########################################################## + print('#### Starting featurewiz transform for train data ####') + X_sel = copy.deepcopy(X) + X_index = X.index + y_index = y.index + ############# This adds a missing flag column for each column ############ + if self.add_missing: + print(' Caution: add_missing adds a missing flag column for every column in your dataset. Beware...') + orig_vars = X_sel.columns.tolist() + X_sel = add_missing(X_sel) + self.missing_flags = left_subtract(X_sel.columns.tolist(), orig_vars) + print(' transformed dataset shape: %s' %(X_sel.shape,)) + ################## This is where we do groupby features ################# + if self.feature_gen: + if np.where('groupby' in self.feature_gen,True, False).tolist(): + if len(self.catvars) >= 1 and len(self.numvars) >= 1: + #### We make sure that only those numvars and catvars in X are used. Not the missing flags! + grp = Groupby_Aggregator(categoricals=self.catvars, aggregates=['mean'], numerics=self.numvars) + X_sel = grp.fit_transform(X_sel) + self.grouper = grp + else: + print('No groupby features created since no categorical or numeric vars in dataset.') + else: + print('No groupby features created since no groupby feature engg specified') + ################## This is where we test for feature interactions ########### + if np.where('interactions' in self.feature_gen,True, False).tolist(): + if len(self.catvars) > 1: + num_combos = len(list(combinations(self.catvars, 2))) + print('Adding %s interactions between %s categorical_vars %s...' %( + num_combos, len(self.catvars), self.catvars)) + #### We make sure that only those numvars and catvars in X are used. Not the missing flags! + X_sel = FE_create_interaction_vars_train(X_sel, self.catvars) + #### Since missing flags are not included in numvars, we are adding them here to select the rest + combovars = left_subtract(X_sel.columns.tolist(), self.numvars+self.missing_flags) + self.combovars = combovars + else: + print('No interactions created for categorical vars since number less than 2') + else: + print('No interactions created for categorical vars since no interactions feature engg specified') + ##### Now put a dataframe together of transformed X and y #### + X_sel.index = X_index + + #### Use lazytransform to transform all variables to numeric ### + X_sel, y_sel = self.lazy.fit_transform(X_sel, y) + + ### Sometimes after imbalanced flag, this index becomes different! + X_index = X_sel.index + y_index = y_sel.index + + #### Now check if y is transformed properly ############### + if isinstance(y_sel, np.ndarray): + if isinstance(y, pd.Series): + y_sel = pd.Series(y_sel, name=self.target, index=y_index) + elif isinstance(y, pd.DataFrame): + y_sel = pd.Series(y_sel, columns=self.targets, index=y_index) + else: + print('y is not formatted correctly: check your input y and try again.') + return self + + ## Fit DAE transformer to the data. This method trains the autoencoder model. + if not self.ae is None: + ### since you cannot fit model before transforming data, leave it here ### + self.ae.fit(X_sel, y_sel) + if 'gan' in self.feature_gen: + print('Fitting and transforming a GAN for each class...') + X_sel_ae, y_sel = self.ae.transform(X_sel, y_sel) + else: + print('Fitting and transforming an Auto Encoder for dataset...') + X_sel_ae = self.ae.transform(X_sel, y_sel) + if np.all(np.isnan(X_sel_ae)): + print('Auto encoder is erroring. Using existing features shape: %s' %(X_sel.shape,)) + else: + ### You need to check for both 'vae' and 'vae_add': this does both!! + if [y for y in self.feature_gen if 'dae' in y] or [y for y in self.feature_gen if 'vae' in y]: + new_vars = ['feature_'+str(x) for x in range(X_sel_ae.shape[1])] + ## Since this results in a higher dimension you need to create new columns ## + X_sel_ae = pd.DataFrame(X_sel_ae, columns=new_vars, index=X_index) + else: + #### This is for GAN only since it doesn't add columns but adds rows! ### + old_rows = X_index.max() + add_rows = len(X_sel_ae) - len(X_index) + new_vars = X_sel.columns + X_index = np.concatenate((X_index, np.arange(old_rows+1, old_rows+add_rows+1))) + X_sel_ae = pd.DataFrame(X_sel_ae, columns=new_vars, index=X_index) + y_index = X_sel_ae.index + y_sel = pd.DataFrame(y_sel, columns=self.targets, index=y_index) + #### Don't change this next line since it applies new rules to above! ### + if 'dae_add' in self.feature_gen or 'vae_add' in self.feature_gen: + ### Only if add is specified do you add the features to X + X_sel = pd.concat([X_sel, X_sel_ae], axis=1) + else: + ### Just replace X_sel with X_sel_ae for all other values ### + X_sel = copy.deepcopy(X_sel_ae) + print('Shape of transformed data due to auto encoder = %s' %(X_sel.shape,)) + ##### Put the dataframe together ####################### + if (X_index == y_index).all(): + if isinstance(X_sel, pd.DataFrame) and (isinstance(y_sel, pd.DataFrame) or isinstance(y_sel, pd.Series)): + df = pd.concat([X_sel, y_sel], axis=1) + else: + print('X and y are not pandas dataframes or series. Check your input and try again') + return X, y + else: + df = pd.concat([X_sel.reset_index(drop=True), y_sel], axis=1) + df.index = X_index + # Select features using featurewiz + self.model_type, self.multi_label_type = analyze_problem_type(df[self.targets], self.targets) + #### This is where you need to drop columns that have zero variance ###### + self.cols_zero_variance = X_sel.columns[(X_sel.var()==0)] + if len(self.cols_zero_variance) > 0: + print(' Dropping %d columns due to zero variance...' %len(self.cols_zero_variance)) + X_sel = X_sel.drop(self.cols_zero_variance, axis=1) + df = df.drop(self.cols_zero_variance, axis=1) + self.numvars = X_sel.columns.tolist() + if not self.skip_sulov: + self.numvars = FE_remove_variables_using_SULOV_method(df, self.numvars, self.model_type, self.targets, + self.corr_limit, self.verbose, self.dask_xgboost_flag) + if not self.skip_xgboost: + print('Performing recursive XGBoost feature selection from %d features...' %len(self.numvars)) + features = FE_perform_recursive_xgboost(df, self.targets, self.model_type, + self.multi_label_type, self.dask_xgboost_flag, self.verbose) + else: + features = copy.deepcopy(self.numvars) + # find the time taken to run feature selection #### + difftime = max(1, np.int16(time.time()-start_time)) + print(' time taken to run entire featurewiz = %s second(s)' %difftime) + # column of labels + self.features = features + print('Recursive XGBoost selected %d features...' %len(self.features)) + return X_sel[self.features], y_sel +################################################################################################### +def EDA_remove_special_chars(df): + """ + This function removes special chars from column names and returns a df with new column names. + Inputs and outputs are both the same dataframe except column names are changed. + """ + import copy + import re + cols = df.columns.tolist() + copy_cols = copy.deepcopy(cols) + ser = pd.Series(cols) + ### This function removes all special chars from a list ### + remove_special_chars = lambda x:re.sub('[^A-Za-z0-9_]+', '', x) + newls = ser.map(remove_special_chars).values.tolist() + df.columns = newls + return df +################################################################################################### +def dask_xgboost_training(X_trainx, y_trainx, params): + + cluster = dask.distributed.LocalCluster() + dask_client = dask.distributed.Client(cluster) + X_trainx = dd.from_pandas(X_train, npartitions=1) + y_trainx = dd.from_pandas(y_train, npartitions=1) + print("DASK XGBoost training...") + dtrain = xgb.dask.DaskDMatrix(dask_client, X_trainx, y_trainx) + bst = xgb.dask.train(dask_client, params, dtrain, num_boost_round=10) + dask_client.close() + print(" training completed...") + return bst +#################################################################################### +def FE_remove_commas_in_numerics(train, nums=[]): + """ + This function removes commas in numeric columns and returns the columns transformed. + You can send in a dataframe with one column name as a string or a list of columns. + Returns a single array if only one column is sent. + Returns the entire dataframe if a list of columns is sent. This includes all columns. + """ + train = copy.deepcopy(train) + if isinstance(nums, str): + return train[each_num].map(lambda x: float("".join( x.split(",")))).values + else: + for each_num in nums: + train[each_num] = train[each_num].map(lambda x: float("".join( x.split(",")))).values + return train +#################################################################################### +### this works only on pandas dataframes but it is extremely fast +import copy +def FE_calculate_duration_from_timestamp(df, id_column, timestamp_column): + """ + ################################################################################### + Calculate the total time and average time spent online per day by user. + Also it calculates the number of logins per user per day. + ### This is very useful for logs data, IOT data, and pings from visits data ##### + This function takes a DataFrame with user ids and timestamps (of logins, etc.) + and returns a DataFrame with duration or the time spent between two timestamps. + This is calculated by taking pairs of rows and assuming the first row is login + and the second row is logout. Then we subtract the timestamp of the login from + the timestamp of the logout for each pair of rows. This function uses alternate rows + of a dataframe and splits them into separate columns. It then subtracts the two + columns to find time delta in seconds during those two times. It also eliminates + any data entry errors by removing negative durations. This is the best and + speediest way to calculate online time spent per user per day. + ################################################################################### + Parameters + ---------- + df : pandas.DataFrame + The input DataFrame with user ids, timestamps and values. + id_column : str + The name of the column that contains the user ids. + timestamp_column : str + Name of the timestamp column + + Returns + ------- + result : pandas.DataFrame + The output DataFrame with user ids, dates, average time spent and number of logins. + """ + df = copy.deepcopy(df) + # Create an empty DataFrame to store the results + columns = [id_column, timestamp_column] + df = df[columns] + leng = len(df) + # Reshape the DataFrame into two columns by stacking every other row + df1 = df.iloc[::2] # select every even row + df2 = df.drop(0, axis=0) # drop the first row + df2 = df2.iloc[::2] # select every even row from the remaining rows + # If length of dataframe is not an even number, process until the last row + if leng%2 != 0: + lastrow = dict(df.iloc[-1]) # get the last row as a dictionary + df2 = df2.append(lastrow, ignore_index=True) # append it to df2 + df1 = df1.rename(columns={timestamp_column:timestamp_column+'_begin'}) # rename the timestamp column in df1 + df2 = df2.rename(columns={timestamp_column:timestamp_column+'_end'}) # rename the timestamp column in df2 + df1x = df1.reset_index(drop=True) # reset the index of df1 + df2x = df2.reset_index(drop=True) # reset the index of df2 + df3 = pd.concat([df1x, df2x], axis=1) # concatenate df1 and df2 horizontally + result = df3.iloc[:,[0,1,3]] # select only the relevant columns from df3 + # calculate the time difference between each pair of rows + result["time_diff"] = result[timestamp_column+"_end"] - result[timestamp_column+"_begin"] + #convert the value_diff column to seconds using np.timedelta64(1, 's') function + result['time_diff'] = result['time_diff'] / np.timedelta64(1, 's') + result.loc[(result['time_diff']<0),"time_diff"] = 0 + + #return the result + return result +############################################################################################### +from pandas.api.types import is_object_dtype +from sklearn.impute import MissingIndicator +def add_missing(df): + """ + #### Missing values indicator - it adds missing flag to all columns in a dataframe ######### + ### It does not make sense to add an indicator when train has no missing values and test does. + ### In such cases, you will have an extra column in test while there won't be in train + ### So it is better to create this extra column for all columns in a dataframe so that + ### train and test data sets have same features when creating feature transformer pipelines. + ############################################################################################## + + """ + df = copy.deepcopy(df) + df_index = df.index + col_names = df.columns + col_names = [x+'_missing' for x in col_names] + if is_object_dtype(df.columns): + miss = MissingIndicator(features="all") + df_add = pd.DataFrame(miss.fit_transform(df).astype(np.int8), index=df_index, columns=col_names) + df = df.join(df_add) + return df + else: + print('Column names must be strings in dataframe. Returning as is...') + return df +############################################################################################### +import copy +from sklearn.model_selection import RepeatedStratifiedKFold, StratifiedKFold, KFold +from lazytransform import print_classification_metrics, print_regression_metrics +def cross_val_model_predictions(model, train, test, targets, modeltype, + feature_engg=[], cv=None, splits=5, feature_selection=False): + """ + Conducts cross-validation and generates predictions using a specified machine learning model. + + This function performs cross-validation on the provided training data and generates predictions for both training + and test datasets. It supports both regression and classification models, including special handling for + XGBoost models. The function allows for different cross-validation strategies and handles feature engineering + and target transformation if required. + + Parameters: + - model: A scikit-learn compatible model object. This can be any model that follows scikit-learn's API. + - train (pd.DataFrame): Training data as a Pandas DataFrame. It should include both features and the target variable. + - test (pd.DataFrame): Test data as a Pandas DataFrame. It should include features but not the target variable. + - targets (list): A list containing the names of the target variable(s) in the train DataFrame. + - modeltype (string): A string defining the type of model whether "Regression", "Binary_Classification" or "Multi-Classification" + - feature_engg (list): default is []. You can add "interactions", "groupby", "target", or all three features to your model. + - cv (Optional): A cross-validation strategy object. If None, KFold with 5 splits is used by default. + - splits: the number of splits to be used in CV strategy. Default is 5 + - feature_selection: To use or not use feature_selection using featurewiz. Default is False. + + The function performs the following steps: + 1. Initializes various variables and sets up the cross-validation strategy. + 2. Performs feature engineering using the FeatureWiz library if necessary. + 3. Trains the model on each fold of the cross-validation and makes predictions. + 4. Evaluates the model performance using appropriate metrics. + 5. Generates predictions for the test set. + + Returns: + - test_preds (np.array): Array of predictions for the test set. + - test_probabs (np.array): Array of prediction probabilities for the test set (if applicable). + + Note: + - The function assumes that the input data frames (train and test) are pre-processed and ready for model training. + - For XGBoost models, the number of estimators is handled automatically. For other models, a default of 200 estimators is used. + - The function uses FeatureWiz for feature selection and transformation, which needs to be installed separately. + + Raises: + - ValueError: If the model type is not recognized or if there are issues with the data input. + + Example usage: + >>> from sklearn.ensemble import RandomForestClassifier + >>> model = RandomForestClassifier() + >>> val_scores, test_preds, test_probabs = cross_val_model_predictions(model, train_df, test_df, + targets=['target'], modeltype='Regression', + feature_engg=['groupby'], feature_selection=False, + cv=None, splits=5) + """ + seed = 42 + enco = 'catboost' + np.random.seed(seed) + X = train.copy(deep=True) + y = X.pop(targets[0]) + test_copy = test.copy(deep=True) + ### define test set ### + X_test = test_copy.drop(targets[0], axis=1) + ### Do this only if the model is XGBoost ### + if str(model).split("(")[0] == 'XGBRegressor' or str(model).split("(")[0] == 'XGBClassifier' : + n_ests = model.get_params()['n_estimators'] + else: + n_ests = 200 + ### if cv is None, just use KFold ### + if cv is None: + if modeltype == 'Regression': + cv = KFold(n_splits = splits, random_state = 99, shuffle = True) + else: + cv = StratifiedKFold(n_splits = splits, random_state = 99, shuffle = True) + + #initiate prediction arrays and score lists + train_predictions = None + val_predictions = None + test_predictions = None + test_probas = None + + train_scores, val_scores = [], [] + + #training model, predicting prognosis probability, and evaluating log loss + for fold, (train_idx, val_idx) in enumerate(cv.split(X, train[targets])): + print('################## Fold %s processing ###############################' %(fold+1)) + #define train set + X_train = X.iloc[train_idx] + y_train = y.iloc[train_idx] + + #define validation set + X_val = X.iloc[val_idx] + y_val = y.iloc[val_idx] + + if feature_selection: + fwiz = FeatureWiz( + corr_limit=0.9, + feature_engg=feature_engg, + category_encoders=enco, + add_missing=False, + nrows=None, + verbose=0, + transform_target=True, + scalers="std", + ) + X_train, y_train = fwiz.fit_transform( + X=X_train, + y=y_train,) + + X_val = fwiz.transform(X_val) + ### This transforms y_test alone without touching X_test. Nice trick! + if modeltype != 'Regression': + y_val = fwiz.lazy.yformer.transform(y_val) + + else: + ### use lazy transform with default settings ########### + lazy = LazyTransformer(model=None, encoders='label', scalers='', + transform_target=True, imbalanced=False, verbose=1) + X_train, y_train = lazy.fit_transform(X_train, y_train) + X_val = lazy.transform(X_val) + ### This transforms y_test alone without touching X_test. Nice trick! + if modeltype != 'Regression': + y_val = lazy.yformer.transform(y_val) + + #train model + if str(model).split("(")[0] == 'XGBRegressor' or str(model).split("(")[0] == 'XGBClassifier' : + model.fit(X_train, y_train, eval_set=[(X_val, y_val)], early_stopping_rounds=int(0.2*n_ests), verbose=0, ) + else: + model.fit(X_train, y_train) + + #make predictions + if modeltype == 'Regression': + train_preds = model.predict(X_train) + val_preds = model.predict(X_val) + else: + train_preds = model.predict_proba(X_train) + val_preds = model.predict_proba(X_val) + + if fold == 0: + if modeltype == 'Regression': + train_predictions = copy.deepcopy(train_preds) + val_predictions = copy.deepcopy(val_preds) + elif modeltype == 'Binary_Classification': + train_predictions = copy.deepcopy(train_preds[:,1]) + val_predictions = copy.deepcopy(val_preds[:,1]) + else: + train_predictions = copy.deepcopy(train_preds) + val_predictions = copy.deepcopy(val_preds) + else: + if modeltype == 'Regression': + train_predictions = np.hstack([train_predictions, train_preds]) + val_predictions = np.hstack([val_predictions, val_preds]) + elif modeltype == 'Binary_Classification': + train_predictions = np.hstack([train_predictions, train_preds[:,1]]) + val_predictions = np.hstack([val_predictions, val_preds[:,1]]) + else: + train_predictions = np.vstack([train_predictions, train_preds]) + val_predictions = np.vstack([val_predictions, val_preds]) + + #evaluate model for a fold + if modeltype == 'Regression': + print('Model results on Train data:') + train_score = print_regression_metrics(y_train, train_preds) + print('Model results on Validation data:') + val_score = print_regression_metrics(y_val, val_preds) + else: + print('Model results on Train data:') + train_score = print_classification_metrics(y_train, model.predict(X_train), train_preds) + print('Model results on Validation data:') + val_score = print_classification_metrics(y_val, model.predict(X_val), val_preds) + + #append model score for a fold to list + train_scores.append(train_score) + val_scores.append(val_score) + + ### make your predictions now + if feature_selection: + X_test_trans = fwiz.transform(X_test) + else: + X_test_trans = lazy.transform(X_test) + + if fold == 0: + if modeltype == 'Regression': + test_predictions = model.predict(X_test_trans) / splits + test_probas = copy.deepcopy(test_predictions) + elif modeltype == 'Binary_Classification': + test_predictions = model.predict(X_test_trans) / splits + test_probas = model.predict_proba(X_test_trans) / splits + else: + test_predictions = model.predict(X_test_trans) / splits + test_probas = model.predict_proba(X_test_trans) / splits + else: + if modeltype == 'Regression': + test_predictions = np.vstack([test_predictions, model.predict(X_test_trans) / splits]) + test_probas = copy.deepcopy(test_predictions) + elif modeltype == 'Binary_Classification': + test_predictions = np.dstack([test_predictions, model.predict(X_test_trans) / splits]) + test_probas = np.dstack([test_probas, model.predict_proba(X_test_trans) / splits]) + else: + test_predictions = np.dstack([test_predictions, model.predict(X_test_trans) / splits]) + test_probas = np.dstack([test_probas, model.predict_proba(X_test_trans) / splits]) + + print(f'Val Scores average: {np.mean(val_scores):.5f} ± {np.std(val_scores):.5f} | Train Scores average: {np.mean(train_scores):.5f} ± {np.std(train_scores):.5f} | {targets}') + if modeltype == 'Regression': + test_preds = np.sum(test_predictions, axis=0) + test_probabs = copy.deepcopy(test_predictions) + elif modeltype == 'Binary_Classification': + test_preds = np.sum(test_predictions, axis=2).astype(int).squeeze() + test_probabs = np.sum(test_probas,axis=2) + else: + test_preds = np.sum(test_predictions, axis=2).astype(int).squeeze() + test_probabs = np.sum(test_probas,axis=2) + return test_preds, test_probabs +######################################################################################################### + diff --git a/build/lib/featurewiz/ml_models.py b/build/lib/featurewiz/ml_models.py new file mode 100644 index 0000000..41c15ff --- /dev/null +++ b/build/lib/featurewiz/ml_models.py @@ -0,0 +1,1818 @@ +import numpy as np +np.random.seed(99) +import random +random.seed(42) +import pandas as pd +################################################################################ +import warnings +warnings.filterwarnings("ignore") +from sklearn.exceptions import DataConversionWarning +warnings.filterwarnings(action='ignore', category=DataConversionWarning) +with warnings.catch_warnings(): + warnings.simplefilter("ignore") +################################################################################# +from sklearn.model_selection import train_test_split +from sklearn.model_selection import KFold +from sklearn.model_selection import GridSearchCV +from sklearn.multioutput import MultiOutputClassifier, MultiOutputRegressor +from sklearn.multiclass import OneVsRestClassifier +import xgboost as xgb +from xgboost.sklearn import XGBClassifier +from xgboost.sklearn import XGBRegressor +from sklearn.model_selection import train_test_split +from sklearn.multiclass import OneVsRestClassifier +from sklearn.preprocessing import LabelEncoder +import lightgbm as lgbm +from sklearn.model_selection import KFold, cross_val_score,StratifiedKFold +import seaborn as sns +from sklearn.preprocessing import OneHotEncoder, LabelEncoder, label_binarize +import csv +import re +from xgboost import XGBRegressor, XGBClassifier +from sklearn.metrics import mean_squared_log_error, mean_squared_error,balanced_accuracy_score +from scipy import stats +from sklearn.model_selection import RandomizedSearchCV +import scipy as sp +import time +import copy +from sklearn.preprocessing import StandardScaler, MinMaxScaler +from collections import Counter, defaultdict +import pdb +from tqdm.notebook import tqdm +from pathlib import Path + +#sklearn data_preprocessing +from sklearn.preprocessing import StandardScaler, MinMaxScaler +#sklearn categorical encoding +import category_encoders as ce +from .my_encoders import My_LabelEncoder + +#sklearn modelling +from sklearn.model_selection import KFold +from collections import Counter, defaultdict +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin + +# boosting library +import xgboost as xgb +import matplotlib.pyplot as plt + +import copy +################################################################################# +#### Regression or Classification type problem +##################################################################################### +from sklearn.impute import SimpleImputer +def data_transform(X_train, Y_train, X_test="", Y_test="", modeltype='Classification', + multi_label=False, enc_method='label', scaler = StandardScaler()): + ##### Use My_Label_Encoder to transform label targets if needed ##### + if multi_label: + if modeltype != 'Regression': + targets = Y_train.columns + Y_train_encoded = copy.deepcopy(Y_train) + for each_target in targets: + if Y_train[each_target].dtype not in ['int64', 'int32','int16','int8', 'float16','float32','float64','float']: + mlb = My_LabelEncoder() + Y_train_encoded[each_target] = mlb.fit_transform(Y_train[each_target]) + if not isinstance(Y_test, str): + Y_test_encoded= mlb.transform(Y_test) + else: + Y_test_encoded = copy.deepcopy(Y_test) + else: + Y_train_encoded = copy.deepcopy(Y_train) + Y_test_encoded = copy.deepcopy(Y_test) + else: + Y_train_encoded = copy.deepcopy(Y_train) + Y_test_encoded = copy.deepcopy(Y_test) + else: + if modeltype != 'Regression': + if Y_train.dtype not in ['int64', 'int32','int16','int8', 'float16','float32','float64','float']: + mlb = My_LabelEncoder() + Y_train_encoded= mlb.fit_transform(Y_train) + if not isinstance(Y_test, str): + Y_test_encoded= mlb.transform(Y_test) + else: + Y_test_encoded = copy.deepcopy(Y_test) + else: + Y_train_encoded = copy.deepcopy(Y_train) + Y_test_encoded = copy.deepcopy(Y_test) + else: + Y_train_encoded = copy.deepcopy(Y_train) + Y_test_encoded = copy.deepcopy(Y_test) + + + #### This is where we find datetime vars and convert them to strings #### + datetime_feats = X_train.select_dtypes(include='datetime').columns.tolist() + ### if there are datetime values, convert them into features here ### + from .featurewiz import FE_create_time_series_features + for date_col in datetime_feats: + fillnum = X_train[date_col].mode()[0] + X_train[date_col].fillna(fillnum,inplace=True) + X_train, ts_adds = FE_create_time_series_features(X_train, date_col) + if not isinstance(X_test, str): + X_test[date_col].fillna(fillnum,inplace=True) + X_test, _ = FE_create_time_series_features(X_test, date_col, ts_adds) + print(' Adding time series features from %s to data...' %date_col) + ####### Set up feature to encode #################### + ##### First make sure that the originals are not modified ########## + X_train_encoded = copy.deepcopy(X_train) + X_test_encoded = copy.deepcopy(X_test) + feature_to_encode = X_train.select_dtypes(include='object').columns.tolist( + )+X_train.select_dtypes(include='category').columns.tolist() + #### Do label encoding now ################# + if enc_method == 'label': + for feat in feature_to_encode: + # Initia the encoder model + lbEncoder = My_LabelEncoder() + fillnum = X_train[feat].mode()[0] + X_train[feat].fillna(fillnum,inplace=True) + # fit the train data + lbEncoder.fit(X_train[feat]) + # transform training set + X_train_encoded[feat] = lbEncoder.transform(X_train[feat]) + # transform test set + if not isinstance(X_test_encoded, str): + X_test[feat].fillna(fillnum,inplace=True) + X_test_encoded[feat] = lbEncoder.transform(X_test[feat]) + elif enc_method == 'glmm': + # Initialize the encoder model + GLMMEncoder = ce.glmm.GLMMEncoder(verbose=0 ,binomial_target=False) + # fit the train data + GLMMEncoder.fit(X_train[feature_to_encode],Y_train_encoded) + # transform training set #### + X_train_encoded[feature_to_encode] = GLMMEncoder.transform(X_train[feature_to_encode]) + # transform test set + if not isinstance(X_test_encoded, str): + X_test_encoded[feature_to_encode] = GLMMEncoder.transform(X_test[feature_to_encode]) + else: + print('No encoding transform performed') + + ### make sure there are no missing values ### + try: + imputer = SimpleImputer(strategy='constant', fill_value=0, verbose=0, add_indicator=True) + imputer.fit_transform(X_train_encoded) + if not isinstance(X_test_encoded, str): + imputer.transform(X_test_encoded) + except: + X_train_encoded = X_train_encoded.fillna(0) + if not isinstance(X_test_encoded, str): + X_test_encoded = X_test_encoded.fillna(0) + + # fit the scaler to the entire train and transform the test set + scaler.fit(X_train_encoded) + # transform training set + X_train_scaled = pd.DataFrame(scaler.transform(X_train_encoded), + columns=X_train_encoded.columns, index=X_train_encoded.index) + # transform test set + if not isinstance(X_test_encoded, str): + X_test_scaled = pd.DataFrame(scaler.transform(X_test_encoded), + columns=X_test_encoded.columns, index=X_test_encoded.index) + else: + X_test_scaled = "" + + return X_train_scaled, Y_train_encoded, X_test_scaled, Y_test_encoded +################################################################################## +from sklearn.model_selection import KFold, cross_val_score,StratifiedKFold +import seaborn as sns +from sklearn.preprocessing import OneHotEncoder, LabelEncoder, label_binarize +import csv +import re +from xgboost import XGBRegressor, XGBClassifier +from sklearn.metrics import mean_squared_log_error, mean_squared_error,balanced_accuracy_score +from scipy import stats +from sklearn.model_selection import RandomizedSearchCV +import scipy as sp +import time +################################################################################## +import lightgbm as lgbm +def lightgbm_model_fit(random_search_flag, x_train, y_train, x_test, y_test, modeltype, + multi_label, log_y, model=""): + start_time = time.time() + if multi_label: + ###### This is for Multi_Label problems ############ + rand_params = {'estimator__learning_rate':[0.1, 0.5, 0.01, 0.05], + 'estimator__n_estimators':[50, 100, 150, 200, 250], + #'estimator__gamma':[0, 2, 4, 8, 16, 32], ## there is no gamma in LGBM models ## + 'estimator__max_depth':[3, 5, 8, 12], + 'estimator__class_weight':[None, 'balanced'] + } + else: + rand_params = { + 'learning_rate': sp.stats.uniform(scale=1), + 'num_leaves': sp.stats.randint(20, 100), + 'n_estimators': sp.stats.randint(100,500), + "max_depth": sp.stats.randint(3, 15), + 'class_weight':[None, 'balanced'] + } + gpu_exists = check_if_GPU_exists() + if modeltype == 'Regression': + if gpu_exists: + lgb = lgbm.LGBMRegressor(device="gpu") + else: + lgb = lgbm.LGBMRegressor() + objective = 'regression' + metric = 'rmse' + is_unbalance = False + class_weight = None + score_name = 'Score' + else: + if modeltype =='Binary_Classification': + if gpu_exists: + lgb = lgbm.LGBMClassifier(device="gpu") + else: + lgb = lgbm.LGBMClassifier() + objective = 'binary' + metric = 'auc' + is_unbalance = True + class_weight = None + score_name = 'ROC AUC' + num_class = 1 + else: + if gpu_exists: + lgb = lgbm.LGBMClassifier(device="gpu") + else: + lgb = lgbm.LGBMClassifier() + objective = 'multiclass' + #objective = 'multiclassova' + metric = 'multi_logloss' + is_unbalance = True + class_weight = 'balanced' + score_name = 'Multiclass Logloss' + if multi_label: + if isinstance(y_train, np.ndarray): + num_class = np.unique(y_train).max() + 1 + else: + num_class = y_train.nunique().max() + else: + if isinstance(y_train, np.ndarray): + num_class = np.unique(y_train).max() + 1 + else: + num_class = y_train.nunique() + + early_stopping_params={"early_stopping_rounds":10, + "eval_metric" : metric, + "eval_set" : [[x_test, y_test]], + } + if modeltype == 'Regression': + ## there is no num_class in regression for LGBM model ## + lgbm_params = { + 'objective': objective, + 'metric': metric, + 'boosting_type': 'gbdt', + 'save_binary': True, + 'seed': 1337, 'feature_fraction_seed': 1337, + 'bagging_seed': 1337, 'drop_seed': 1337, + 'data_random_seed': 1337, + 'verbose': -1, + 'n_estimators': 400, + } + else: + if multi_label: + ### If it is multi_label, having fewer params help avoid errors ## + ### Also LGBM doesn't work well when there are binary and multiclass mixed in multi-labels ## + lgbm_params = { + 'boosting_type': 'gbdt', + 'seed': 1337, 'feature_fraction_seed': 1337, + 'bagging_seed': 1337, 'drop_seed': 1337, + 'data_random_seed': 1337, + 'verbose': -1, + 'n_estimators': 400, + } + else: + lgbm_params = { + 'objective': objective, + 'metric': metric, + 'boosting_type': 'gbdt', + 'save_binary': True, + 'seed': 1337, 'feature_fraction_seed': 1337, + 'bagging_seed': 1337, 'drop_seed': 1337, + 'data_random_seed': 1337, + 'verbose': -1, + 'num_class': num_class, + 'is_unbalance': is_unbalance, + 'class_weight': class_weight, + 'n_estimators': 400, + } + ### Don't change the next line. It has to be lgb to refer to the model!! + lgb.set_params(**lgbm_params) + if multi_label: + if modeltype == 'Regression': + lgb = MultiOutputRegressor(lgb) + else: + lgb = MultiOutputClassifier(lgb) + if random_search_flag: + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + else: + scoring = 'precision' + model = RandomizedSearchCV(lgb, + param_distributions = rand_params, + n_iter = 15, + return_train_score = True, + random_state = 99, + n_jobs=-1, + cv = 3, + refit=True, + scoring = scoring, + verbose = False) + model.fit(x_train, y_train) + print('Time taken for Hyper Param tuning of multi_label LightGBM (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + cv_results = pd.DataFrame(model.cv_results_) + if modeltype == 'Regression': + print('Mean cross-validated train %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_train_score'].mean())))) + print('Mean cross-validated test %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_test_score'].mean())))) + else: + print('Mean cross-validated train %s = %0.04f' %(score_name, cv_results['mean_train_score'].mean())) + print('Mean cross-validated test %s = %0.04f' %(score_name, cv_results['mean_test_score'].mean())) + ### In this case, there is no boost rounds so just return the default num_boost_round + return model.best_estimator_ + else: + try: + model.fit(x_train, y_train) + except: + print('Multi_label LightGBM model is crashing during training. Please check your inputs and try again...') + return model + else: + ######## Single Label problems ############ + if random_search_flag: + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + else: + scoring = 'precision' + model = RandomizedSearchCV(lgb, + param_distributions = rand_params, + n_iter = 10, + return_train_score = True, + random_state = 99, + n_jobs=-1, + cv = 3, + refit=True, + scoring = scoring, + verbose = False) + ##### This is where we search for hyper params for model ####### + if multi_label: + model.fit(x_train, y_train) + else: + model.fit(x_train, y_train, **early_stopping_params) + print('Time taken for Hyper Param tuning of LGBM (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + cv_results = pd.DataFrame(model.cv_results_) + if modeltype == 'Regression': + print('Mean cross-validated train %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_train_score'].mean())))) + print('Mean cross-validated test %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_test_score'].mean())))) + else: + print('Mean cross-validated train %s = %0.04f' %(score_name, cv_results['mean_train_score'].mean())) + print('Mean cross-validated test %s = %0.04f' %(score_name, cv_results['mean_test_score'].mean())) + else: + try: + model.fit(x_train, y_train, verbose=-1) + except: + print('lightgbm model is crashing. Please check your inputs and try again...') + return model +############################################################################################## +import os +def check_if_GPU_exists(verbose=0): + try: + os.environ['NVIDIA_VISIBLE_DEVICES'] + if verbose: + print('GPU active on this device') + return True + except: + if verbose: + print('No GPU active on this device') + return False +############################################################################################# +def complex_XGBoost_model(X_train, y_train, X_test, log_y=False, GPU_flag=False, + scaler = '', enc_method='label', n_splits=5, verbose=0): + """ + This model is called complex because it handle multi-label, mulit-class datasets which XGBoost ordinarily cant. + Just send in X_train, y_train and what you want to predict, X_test + It will automatically split X_train into multiple folds (10) and train and predict each time on X_test. + It will then use average (or use mode) to combine the results and give you a y_test. + It will automatically detect modeltype as "Regression" or 'Classification' + It will also add MultiOutputClassifier and MultiOutputRegressor to multi_label problems. + The underlying estimators in all cases is XGB. So you get the best of both worlds. + + Inputs: + ------------ + X_train: pandas dataframe only: do not send in numpy arrays. This is the X_train of your dataset. + y_train: pandas Series or DataFrame only: do not send in numpy arrays. This is the y_train of your dataset. + X_test: pandas dataframe only: do not send in numpy arrays. This is the X_test of your dataset. + log_y: default = False: If True, it means use the log of the target variable "y" to train and test. + GPU_flag: if your machine has a GPU set this flag and it will use XGBoost GPU to speed up processing. + scaler : default is empty string which means to use StandardScaler. + But you can explicity send in "minmax' to select MinMaxScaler(). + Alternatively, you can send in a scaler object that you define here: MaxAbsScaler(), etc. + enc_method: default is 'label' encoding. But you can choose 'glmm' as an alternative. But those are the only two. + verbose: default = 0. Choosing 1 will give you lot more output. + + Outputs: + ------------ + y_preds: Predicted values for your X_XGB_test dataframe. + It has been averaged after repeatedly predicting on X_XGB_test. So likely to be better than one model. + """ + X_XGB = copy.deepcopy(X_train) + Y_XGB = copy.deepcopy(y_train) + X_XGB_test = copy.deepcopy(X_test) + #################################### + start_time = time.time() + top_num = 10 + num_boost_round = 400 + if isinstance(Y_XGB, pd.Series): + targets = [Y_XGB.name] + elif isinstance(Y_XGB, np.ndarray): + print(' y input is an numpy array and hence convert into a series or dataframe and re-try.') + return + else: + targets = Y_XGB.columns.tolist() + if len(targets) == 1: + multi_label = False + if isinstance(Y_XGB, pd.DataFrame): + Y_XGB = pd.Series(Y_XGB.values.ravel(),name=targets[0], index=Y_XGB.index) + else: + multi_label = True + modeltype, _ = analyze_problem_type(Y_XGB, targets) + ### XGBoost ##### + if modeltype == 'Binary_Classification': + print('# XGBoost is a good choice since it is best for binary classification problems.') + elif modeltype == 'Multi_Classification': + print('# XGBoost is a poor choice for this problem since LightGBM is better for multi-class') + else: + print('# Simple XGBoost is better than Complex_XGBoost for Regression problems') + + columns = X_XGB.columns + ################################################################################### + ######### S C A L E R P R O C E S S I N G B E G I N S ############ + ################################################################################### + if isinstance(scaler, str): + if not scaler == '': + scaler = scaler.lower() + if scaler == 'standard': + scaler = StandardScaler() + elif scaler == 'minmax': + scaler = MinMaxScaler() + else: + scaler = StandardScaler() + else: + scaler = StandardScaler() + else: + pass + ################################################################################# + if modeltype == 'Regression': + if log_y: + Y_XGB.loc[Y_XGB==0] = 1e-15 ### just set something that is zero to a very small number + + ######### Now set the number of rows we need to tune hyper params ### + scoreFunction = { "precision": "precision_weighted","recall": "recall_weighted"} + random_search_flag = True + + #### We need a small validation data set for hyper-param tuning ######################### + hyper_frac = 0.2 + #### now select a random sample from X_XGB ## + if modeltype == 'Regression': + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999) + else: + try: + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999, stratify = Y_XGB) + except: + ## In some small cases there are too few samples to stratify hence just split them as is + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999) + ###### This step is needed for making sure y is transformed to log_y #################### + if modeltype == 'Regression' and log_y: + Y_train = np.log(Y_train) + Y_valid = np.log(Y_valid) + + #### First convert test data into numeric using train data ### + X_train, Y_train, X_valid, Y_valid = data_transform(X_train, Y_train, X_valid, Y_valid, + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + + + ###### Time to hyper-param tune model using randomizedsearchcv and partial train data ######### + num_boost_round = xgbm_model_fit(random_search_flag, X_train, Y_train, X_valid, Y_valid, modeltype, + multi_label, log_y, num_boost_round=num_boost_round) + + #### First convert test data into numeric using train data ############################### + if not isinstance(X_XGB_test, str): + x_train, y_train, x_test, _ = data_transform(X_XGB, Y_XGB, X_XGB_test, "", + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + + ###### Time to train the hyper-tuned model on full train data ########################## + random_search_flag = False + model = xgbm_model_fit(random_search_flag, x_train, y_train, x_test, "", modeltype, + multi_label, log_y, num_boost_round=num_boost_round) + + ############# Time to get feature importances based on full train data ################ + + if multi_label: + for i,target_name in enumerate(targets): + each_model = model.estimators_[i] + imp_feats = dict(zip(x_train.columns, each_model.feature_importances_)) + importances = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].values + important_features = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].index.tolist() + print('Top 10 features for {}: {}'.format(target_name, important_features)) + else: + imp_feats = model.get_score(fmap='', importance_type='gain') + importances = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].values + important_features = pd.Series(imp_feats).sort_values(ascending=False)[:top_num].index.tolist() + print('Top 10 features:\n%s' %important_features[:top_num]) + ####### order this in the same order in which they were collected ###### + feature_importances = pd.DataFrame(importances, + index = important_features, + columns=['importance']) + + ###### Time to consolidate the predictions on test data ################################ + if not multi_label and not isinstance(X_XGB_test, str): + x_test = xgb.DMatrix(x_test) + if isinstance(X_XGB_test, str): + print('No predictions since X_XGB_test is empty string. Returning...') + return {} + + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + if log_y: + pred_xgbs = np.exp(model.predict(x_test)) + else: + pred_xgbs = model.predict(x_test) + #### if there is no test data just return empty strings ### + else: + pred_xgbs = [] + else: + if multi_label: + pred_xgbs = model.predict(x_test) + pred_probas = model.predict_proba(x_test) + else: + pred_probas = model.predict(x_test) + if modeltype =='Multi_Classification': + pred_xgbs = pred_probas.argmax(axis=1) + else: + pred_xgbs = (pred_probas>0.5).astype(int) + ##### once the entire model is trained on full train data ################## + print(' Time taken for training XGBoost on entire train data (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + if multi_label: + for i,target_name in enumerate(targets): + each_model = model.estimators_[i] + xgb.plot_importance(each_model, importance_type='gain', max_num_features=top_num, + title='XGBoost model feature importances for %s' %target_name) + else: + xgb.plot_importance(model, importance_type='gain', max_num_features=top_num, + title='XGBoost final model feature importances') + print('Returning the following:') + print(' Model = %s' %model) + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + print(' final predictions', pred_xgbs[:10]) + return (pred_xgbs, model) + else: + if not isinstance(X_XGB_test, str): + print(' final predictions (may need to be transformed to original labels)', pred_xgbs[:10]) + if isinstance(pred_probas, list): + print(' predicted probabilities shape = [(%s, %s)...]' + %(pred_probas[0].shape[0],pred_probas[0].shape[1])) + else: + print(' predicted probabilities', pred_probas[:4]) + return (pred_xgbs, pred_probas, model) +############################################################################################## +import xgboost as xgb +def xgbm_model_fit(random_search_flag, x_train, y_train, x_test, y_test, modeltype, + multi_label, log_y, num_boost_round=100): + start_time = time.time() + if multi_label and not random_search_flag: + model = num_boost_round + else: + rand_params = { + 'learning_rate': sp.stats.uniform(scale=1), + 'gamma': sp.stats.randint(0, 32), + 'n_estimators': sp.stats.randint(100,500), + "max_depth": sp.stats.randint(3, 15), + 'class_weight':[None, 'balanced'], + } + ##### Set the params for GPU and CPU here ### + tree_method = 'hist' + if check_if_GPU_exists(): + tree_method = 'gpu_hist' + ###### This is where we set the default parameters ########### + if modeltype == 'Regression': + objective = 'reg:squarederror' + eval_metric = 'rmse' + shuffle = False + stratified = False + num_class = 0 + score_name = 'Score' + scale_pos_weight = 1 + else: + if modeltype =='Binary_Classification': + objective='binary:logistic' + eval_metric = 'auc' ## dont change this. AUC works well. + shuffle = True + stratified = True + num_class = 1 + score_name = 'AUC' + scale_pos_weight = get_scale_pos_weight(y_train) + else: + objective = 'multi:softprob' + eval_metric = 'auc' ## dont change this. AUC works well for now. + shuffle = True + stratified = True + if multi_label: + num_class = y_train.nunique().max() + else: + if isinstance(y_train, np.ndarray): + num_class = np.unique(y_train).max() + 1 + elif isinstance(y_train, pd.Series): + num_class = y_train.nunique() + else: + num_class = y_train.nunique().max() + score_name = 'Multiclass AUC' + scale_pos_weight = 1 ### use sample_weights in multi-class settings ## + ###################################################### + final_params = { + 'booster' :'gbtree', + 'random_state': 99, + 'objective': objective, + 'eval_metric': eval_metric, + 'tree_method': tree_method, + 'verbosity': 0, + 'n_jobs': -1, + 'scale_pos_weight':scale_pos_weight, + 'num_class': num_class, + 'silent': True + } + ####### This is where we split into single and multi label ############ + if multi_label: + ###### This is for Multi_Label problems ############ + rand_params = {'estimator__learning_rate':[0.1, 0.5, 0.01, 0.05], + 'estimator__n_estimators':[50, 100, 150, 200, 250], + 'estimator__gamma':[0, 2, 4, 8, 16, 32], + 'estimator__max_depth':[3, 5, 8, 12], + 'estimator__class_weight':[None, 'balanced'] + } + if random_search_flag: + if modeltype == 'Regression': + clf = XGBRegressor(n_jobs=-1, random_state=999, max_depth=6) + clf.set_params(**final_params) + model = MultiOutputRegressor(clf, n_jobs=-1) + else: + clf = XGBClassifier(n_jobs=-1, random_state=999, max_depth=6) + clf.set_params(**final_params) + model = MultiOutputClassifier(clf, n_jobs=-1) + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + else: + scoring = 'precision' + model = RandomizedSearchCV(model, + param_distributions = rand_params, + n_iter = 15, + return_train_score = True, + random_state = 99, + n_jobs=-1, + cv = 3, + refit=True, + scoring = scoring, + verbose = False) + model.fit(x_train, y_train) + print('Time taken for Hyper Param tuning of multi_label XGBoost (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + cv_results = pd.DataFrame(model.cv_results_) + if modeltype == 'Regression': + print('Mean cross-validated train %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_train_score'].mean())))) + print('Mean cross-validated test %s = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_test_score'].mean())))) + else: + print('Mean cross-validated train %s = %0.04f' %(score_name, cv_results['mean_train_score'].mean())) + print('Mean cross-validated test %s = %0.04f' %(score_name, cv_results['mean_test_score'].mean())) + ### In this case, there is no boost rounds so just return the default num_boost_round + return model.best_estimator_ + else: + try: + model.fit(x_train, y_train) + except: + print('Multi_label XGBoost model is crashing during training. Please check your inputs and try again...') + return model + else: + #### This is for Single Label Problems ############# + if modeltype == 'Multi_Classification': + wt_array = get_sample_weight_array(y_train) + dtrain = xgb.DMatrix(x_train, label=y_train, weight=wt_array) + else: + dtrain = xgb.DMatrix(x_train, label=y_train) + ######## Now let's perform randomized search to find best hyper parameters ###### + if random_search_flag: + cv_results = xgb.cv(final_params, dtrain, num_boost_round=400, nfold=5, + stratified=stratified, metrics=eval_metric, early_stopping_rounds=10, seed=999, shuffle=shuffle) + # Update best eval_metric + best_eval = 'test-'+eval_metric+'-mean' + if modeltype == 'Regression': + mean_mae = cv_results[best_eval].min() + boost_rounds = cv_results[best_eval].argmin() + else: + mean_mae = cv_results[best_eval].max() + boost_rounds = cv_results[best_eval].argmax() + print("Cross-validated %s = %0.3f in num rounds = %s" %(score_name, mean_mae, boost_rounds)) + print('Time taken for Hyper Param tuning of XGBoost (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + return boost_rounds + else: + try: + model = xgb.train( + final_params, + dtrain, + num_boost_round=num_boost_round, + verbose_eval=False, + ) + except: + print('XGBoost model is crashing. Please check your inputs and try again...') + return model +#################################################################################### +# Calculate class weight +from sklearn.utils.class_weight import compute_class_weight +import copy +from collections import Counter +def find_rare_class(classes, verbose=0): + ######### Print the % count of each class in a Target variable ##### + """ + Works on Multi Class too. Prints class percentages count of target variable. + It returns the name of the Rare class (the one with the minimum class member count). + This can also be helpful in using it as pos_label in Binary and Multi Class problems. + """ + counts = OrderedDict(Counter(classes)) + total = sum(counts.values()) + if verbose >= 1: + print(' Class -> Counts -> Percent') + sorted_keys = sorted(counts.keys()) + for cls in sorted_keys: + print("%12s: % 7d -> % 5.1f%%" % (cls, counts[cls], counts[cls]/total*100)) + if type(pd.Series(counts).idxmin())==str: + return pd.Series(counts).idxmin() + else: + return int(pd.Series(counts).idxmin()) +################################################################################### +def get_sample_weight_array(y_train): + y_train = copy.deepcopy(y_train) + if isinstance(y_train, np.ndarray): + y_train = pd.Series(y_train) + elif isinstance(y_train, pd.Series): + pass + elif isinstance(y_train, pd.DataFrame): + ### if it is a dataframe, return only if it s one column dataframe ## + y_train = y_train.iloc[:,0] + else: + ### if you cannot detect the type or if it is a multi-column dataframe, ignore it + return None + classes = np.unique(y_train) + class_weights = compute_class_weight('balanced', classes=classes, y=y_train) + if len(class_weights[(class_weights < 1)]) > 0: + ### if the weights are less than 1, then divide them until the lowest weight is 1. + class_weights = class_weights/min(class_weights) + else: + class_weights = (class_weights) + ### even after you change weights if they are all below 1.5 do this ## + #if (class_weights<=1.5).all(): + # class_weights = np.around(class_weights+0.49) + class_weights = class_weights.astype(int) + wt = dict(zip(classes, class_weights)) + + ### Map class weights to corresponding target class values + ### You have to make sure class labels have range (0, n_classes-1) + wt_array = y_train.map(wt) + #set(zip(y_train, wt_array)) + + # Convert wt series to wt array + wt_array = wt_array.values + return wt_array +############################################################################### +from collections import OrderedDict +from collections import Counter +from sklearn.utils.class_weight import compute_class_weight +import copy +def get_class_weights(y_input): + ### get_class_weights has lower ROC_AUC but higher F1 scores than get_class_distribution + y_input = copy.deepcopy(y_input) + if isinstance(y_input, np.ndarray): + class_weights = compute_class_weight('balanced', classes=np.unique(y_input), y=y_input.reshape(-1)) + elif isinstance(y_input, pd.Series): + class_weights = compute_class_weight('balanced', classes=np.unique(y_input.values), y=y_input.values.reshape(-1)) + elif isinstance(y_input, pd.DataFrame): + ### if it is a dataframe, return only if it s one column dataframe ## + y_input = y_input.iloc[:,0] + class_weights = compute_class_weight('balanced', classes=np.unique(y_input.values), y=y_input.values.reshape(-1)) + else: + ### if you cannot detect the type or if it is a multi-column dataframe, ignore it + return None + classes = np.unique(y_input) + xp = Counter(y_input) + if len(class_weights[(class_weights < 1)]) > 0: + ### if the weights are less than 1, then divide them until the lowest weight is 1. + class_weights = class_weights/min(class_weights) + else: + class_weights = (class_weights) + ### This is the best version that returns correct weights ### + class_weights = class_weights.astype(int) + class_weights[(class_weights<1)]=1 + class_weights_dict_corrected = dict(zip(classes,class_weights)) + return class_weights_dict_corrected +################################################################################## +from collections import OrderedDict +def get_scale_pos_weight(y_input): + class_weighted_rows = get_class_weights(y_input) + rare_class = find_rare_class(y_input) + rare_class_weight = class_weighted_rows[rare_class] + print(' For class %s, weight = %s' %(rare_class, rare_class_weight)) + return rare_class_weight +############################################################################################ +def xgboost_model_fit(model, x_train, y_train, x_test, y_test, modeltype, log_y, params, + cpu_params, early_stopping_params={}): + early_stopping = 10 + start_time = time.time() + if str(model).split("(")[0] == 'RandomizedSearchCV': + #### we need to set the xgboost version fixed at 1.5 otherwise error! + model.fit(x_train, y_train, **early_stopping_params) + print('Time taken for Hyper Param tuning of XGB (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + else: + try: + if modeltype == 'Regression': + if log_y: + model.fit(x_train, np.log(y_train), early_stopping_rounds=early_stopping, eval_metric=['rmse'], + eval_set=[(x_test, np.log(y_test))], verbose=0) + else: + model.fit(x_train, y_train, early_stopping_rounds=early_stopping, eval_metric=['rmse'], + eval_set=[(x_test, y_test)], verbose=0) + else: + if modeltype == 'Binary_Classification': + objective='binary:logistic' + eval_metric = 'auc' + else: + objective='multi:softprob' + eval_metric = 'auc' + model.fit(x_train, y_train, early_stopping_rounds=early_stopping, eval_metric = eval_metric, + eval_set=[(x_test, y_test)], verbose=0) + except: + print('GPU is present but not turned on. Please restart after that. Currently using CPU...') + if str(model).split("(")[0] == 'RandomizedSearchCV': + xgb = model.estimator_ + xgb.set_params(**cpu_params) + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + else: + scoring = 'precision' + model = RandomizedSearchCV(xgb, + param_distributions = params, + n_iter = 15, + n_jobs=-1, + cv = 3, + scoring=scoring, + refit=True, + ) + model.fit(x_train, y_train, **early_stopping_params) + return model + else: + model = model.set_params(**cpu_params) + if modeltype == 'Regression': + if log_y: + model.fit(x_train, np.log(y_train), early_stopping_rounds=6, eval_metric=['rmse'], + eval_set=[(x_test, np.log(y_test))], verbose=0) + else: + model.fit(x_train, y_train, early_stopping_rounds=6, eval_metric=['rmse'], + eval_set=[(x_test, y_test)], verbose=0) + else: + model.fit(x_train, y_train, early_stopping_rounds=6,eval_metric=eval_metric, + eval_set=[(x_test, y_test)], verbose=0) + return model +################################################################################# +def simple_XGBoost_model(X_train, y_train, X_test, log_y=False, GPU_flag=False, + scaler = '', enc_method='label', n_splits=5, verbose=0): + """ + Easy to use XGBoost model. Just send in X_train, y_train and what you want to predict, X_test + It will automatically split X_train into multiple folds (10) and train and predict each time on X_test. + It will then use average (or use mode) to combine the results and give you a y_test. + You just need to give the modeltype as "Regression" or 'Classification' + + Inputs: + ------------ + X_train: pandas dataframe only: do not send in numpy arrays. This is the X_train of your dataset. + y_train: pandas Series or DataFrame only: do not send in numpy arrays. This is the y_train of your dataset. + X_test: pandas dataframe only: do not send in numpy arrays. This is the X_test of your dataset. + modeltype: can only be 'Regression' or 'Classification' + log_y: default = False: If True, it means use the log of the target variable "y" to train and test. + GPU_flag: if your machine has a GPU set this flag and it will use XGBoost GPU to speed up processing. + scaler : default is StandardScaler(). But you can send in MinMaxScaler() as input to change it or any other scaler. + enc_method: default is 'label' encoding. But you can choose 'glmm' as an alternative. But those are the only two. + verbose: default = 0. Choosing 1 will give you lot more output. + + Outputs: + ------------ + y_preds: Predicted values for your X_XGB_test dataframe. + It has been averaged after repeatedly predicting on X_XGB_test. So likely to be better than one model. + """ + X_XGB = copy.deepcopy(X_train) + Y_XGB = copy.deepcopy(y_train) + X_XGB_test = copy.deepcopy(X_test) + start_time = time.time() + if isinstance(Y_XGB, pd.Series): + targets = [Y_XGB.name] + elif isinstance(Y_XGB, np.ndarray): + print(' y input is an numpy array and hence convert into a series or dataframe and re-try.') + return + else: + targets = Y_XGB.columns.tolist() + Y_XGB_index = Y_XGB.index + if len(targets) == 1: + multi_label = False + if isinstance(Y_XGB, pd.DataFrame): + Y_XGB = pd.Series(Y_XGB.values.ravel(),name=targets[0], index=Y_XGB.index) + else: + multi_label = True + print('Multi_label is not supported in simple_XGBoost_model. Try the complex_XGBoost_model...Returning') + return {} + ##### Start your analysis of the data ############ + modeltype, _ = analyze_problem_type(Y_XGB, targets) + ### XGBoost ##### + if modeltype == 'Binary_Classification': + print('# XGBoost is a good choice since it is better for binary classification problems.') + elif modeltype == 'Multi_Classification': + print('# Avoid XGBoost for this problem since LightGBM is better for multi-class than XGBoost.') + else: + print('# Simple XGBoost is a good choice compared to complex_XGBoost_model for Regression problems.') + + columns = X_XGB.columns + ################################################################################### + ######### S C A L E R P R O C E S S I N G B E G I N S ############ + ################################################################################### + if isinstance(scaler, str): + if not scaler == '': + scaler = scaler.lower() + if scaler == 'standard': + scaler = StandardScaler() + elif scaler == 'minmax': + scaler = MinMaxScaler() + else: + scaler = StandardScaler() + else: + scaler = StandardScaler() + else: + pass + ######### G P U P R O C E S S I N G B E G I N S ############ + ###### This is where we set the CPU and GPU parameters for XGBoost + if GPU_flag: + GPU_exists = check_if_GPU_exists(verbose) + else: + GPU_exists = False + ##### Set the Scoring Parameters here based on each model and preferences of user ### + cpu_params = {} + param = {} + tree_method = 'hist' + if GPU_exists: + tree_method = 'gpu_hist' + cpu_params['tree_method'] = 'hist' + cpu_params['gpu_id'] = 0 + cpu_params['updater'] = 'grow_colmaker' + cpu_params['predictor'] = 'cpu_predictor' + if GPU_exists: + param['tree_method'] = 'gpu_hist' + param['gpu_id'] = 0 + param['updater'] = 'grow_gpu_hist' #'prune' + param['predictor'] = 'gpu_predictor' + print(' Hyper Param Tuning XGBoost with GPU parameters. This will take time. Please be patient...') + else: + param = copy.deepcopy(cpu_params) + print(' Hyper Param Tuning XGBoost with CPU parameters. This will take time. Please be patient...') + ################################################################################# + if modeltype == 'Regression': + if log_y: + Y_XGB.loc[Y_XGB==0] = 1e-15 ### just set something that is zero to a very small number + xgb = XGBRegressor( + booster = 'gbtree', + colsample_bytree=0.5, + alpha=0.015, + gamma=4, + learning_rate=0.01, + max_depth=8, + min_child_weight=2, + n_estimators=1000, + reg_lambda=0.5, + #reg_alpha=8, + subsample=0.7, + random_state=99, + objective='reg:squarederror', + eval_metric='rmse', + verbosity = 0, + n_jobs=-1, + tree_method=tree_method, + silent = True) + objective='reg:squarederror' + eval_metric = 'rmse' + score_name = 'RMSE' + else: + if multi_label: + num_class = Y_XGB.nunique().max() + else: + if isinstance(Y_XGB, np.ndarray): + num_class = np.unique(Y_XGB).max() + 1 + else: + num_class = Y_XGB.nunique() + if num_class == 2: + num_class = 1 + if num_class <= 2: + objective='binary:logistic' + eval_metric = 'auc' + score_name = 'ROC AUC' + else: + objective='multi:softprob' + eval_metric = 'auc' + score_name = 'Multiclass ROC AUC' + xgb = XGBClassifier( + booster = 'gbtree', + colsample_bytree=0.5, + alpha=0.015, + gamma=4, + learning_rate=0.01, + max_depth=8, + min_child_weight=2, + n_estimators=1000, + reg_lambda=0.5, + objective=objective, + subsample=0.7, + random_state=99, + n_jobs=-1, + tree_method=tree_method, + num_class = num_class, + verbosity = 0, + silent = True) + + #testing for GPU + model = xgb.set_params(**param) + hyper_frac = 0.2 + #### now select a random sample from X_XGB and Y_XGB ################ + if modeltype == 'Regression': + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=99) + else: + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=99, stratify=Y_XGB) + + scoreFunction = { "precision": "precision_weighted","recall": "recall_weighted"} + params = { + 'learning_rate': sp.stats.uniform(scale=1), + 'gamma': sp.stats.randint(0, 32), + 'n_estimators': sp.stats.randint(100,500), + "max_depth": sp.stats.randint(3, 15), + } + + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + else: + scoring = 'precision' + model = RandomizedSearchCV(xgb.set_params(**param), + param_distributions = params, + n_iter = 15, + return_train_score = True, + random_state = 99, + n_jobs=-1, + cv = 3, + refit=True, + scoring=scoring, + verbose = False) + + X_train, Y_train, X_valid, Y_valid = data_transform(X_train, Y_train, X_valid, Y_valid, + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + + #### Don't move this. It has to be done after you transform Y_valid to numeric ######## + early_stopping_params={"early_stopping_rounds":5, + "eval_metric" : eval_metric, + "eval_set" : [[X_valid, Y_valid]] + } + gbm_model = xgboost_model_fit(model, X_train, Y_train, X_valid, Y_valid, modeltype, + log_y, params, cpu_params, early_stopping_params) + ############################################################################# + ls=[] + if modeltype == 'Regression': + fold = KFold(n_splits=n_splits) + else: + fold = StratifiedKFold(shuffle=True, n_splits=n_splits, random_state=99) + scores=[] + if not isinstance(X_XGB_test, str): + pred_xgbs = np.zeros(len(X_XGB_test)) + pred_probas = np.zeros(len(X_XGB_test)) + else: + pred_xgbs = [] + pred_probas = [] + #### First convert test data into numeric using train data ### + if not isinstance(X_XGB_test, str): + X_XGB_train_enc, Y_XGB, X_XGB_test_enc, _ = data_transform(X_XGB, Y_XGB, X_XGB_test,"", + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + else: + X_XGB_train_enc, Y_XGB, X_XGB_test_enc, _ = data_transform(X_XGB, Y_XGB, "","", + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + #### now run all the folds each one by one ################################## + start_time = time.time() + for folds, (train_index, test_index) in tqdm(enumerate(fold.split(X_XGB,Y_XGB))): + + x_train, x_valid = X_XGB.iloc[train_index], X_XGB.iloc[test_index] + + ### you need to keep y_valid as-is in the same original state as it was given #### + if isinstance(Y_XGB, np.ndarray): + Y_XGB = pd.Series(Y_XGB,name=targets[0], index=Y_XGB_index) + ### y_valid here will be transformed into log_y to ensure training and validation #### + + if modeltype == 'Regression': + if log_y: + y_train, y_valid = np.log(Y_XGB.iloc[train_index]), np.log(Y_XGB.iloc[test_index]) + else: + y_train, y_valid = Y_XGB.iloc[train_index], Y_XGB.iloc[test_index] + else: + + y_train, y_valid = Y_XGB.iloc[train_index], Y_XGB.iloc[test_index] + + ## scale the x_train and x_valid values - use all columns - + x_train, y_train, x_valid, y_valid = data_transform(x_train, y_train, x_valid, y_valid, + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + + model = gbm_model.best_estimator_ + model = xgboost_model_fit(model, x_train, y_train, x_valid, y_valid, modeltype, + log_y, params, cpu_params) + + #### now make predictions on validation data and compare it to y_valid which is in original state ## + if modeltype == 'Regression': + if log_y: + preds = np.exp(model.predict(x_valid)) + else: + preds = model.predict(x_valid) + else: + preds = model.predict(x_valid) + + feature_importances = pd.DataFrame(model.feature_importances_, + index = X_XGB.columns, + columns=['importance']) + sum_all=feature_importances.values + ls.append(sum_all) + ###### Time to consolidate the predictions on test data ######### + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + if log_y: + pred_xgb=np.exp(model.predict(X_XGB_test_enc[columns])) + else: + pred_xgb=model.predict(X_XGB_test_enc[columns]) + pred_xgbs = np.vstack([pred_xgbs, pred_xgb]) + pred_xgbs = pred_xgbs.mean(axis=0) + #### preds here is for only one fold and we are comparing it to original y_valid #### + score = np.sqrt(mean_squared_error(y_valid, preds)) + print('%s score in fold %d = %s' %(score_name, folds+1, score)) + else: + if not isinstance(X_XGB_test, str): + pred_xgb=model.predict(X_XGB_test_enc[columns]) + pred_proba = model.predict_proba(X_XGB_test_enc[columns]) + if folds == 0: + pred_xgbs = copy.deepcopy(pred_xgb) + pred_probas = copy.deepcopy(pred_proba) + else: + pred_xgbs = np.vstack([pred_xgbs, pred_xgb]) + pred_xgbs = stats.mode(pred_xgbs, axis=0)[0][0] + pred_probas = np.mean( np.array([ pred_probas, pred_proba ]), axis=0 ) + #### preds here is for only one fold and we are comparing it to original y_valid #### + score = balanced_accuracy_score(y_valid, preds) + print('%s score in fold %d = %0.1f%%' %(score_name, folds+1, score*100)) + scores.append(score) + print(' Time taken for Cross Validation of XGBoost (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + print("\nCross-validated Average scores are: ", np.sum(scores)/len(scores)) + ##### Train on full train data set and predict ################################# + print('Training model on full train dataset...') + start_time1 = time.time() + model = gbm_model.best_estimator_ + model.fit(X_XGB_train_enc, Y_XGB) + if not isinstance(X_XGB_test, str): + pred_xgbs = model.predict(X_XGB_test_enc) + if modeltype != 'Regression': + pred_probas = model.predict_proba(X_XGB_test_enc) + else: + pred_probas = np.array([]) + else: + pred_xgbs = np.array([]) + pred_probas = np.array([]) + print(' Time taken for training XGBoost (in minutes) = %0.1f' %((time.time()-start_time1)/60)) + if verbose: + plot_importances_XGB(train_set=X_XGB, labels=Y_XGB, ls=ls, y_preds=pred_xgbs, + modeltype=modeltype, top_num='all') + print('Returning the following:') + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + print(' final predictions', pred_xgbs[:10]) + else: + print(' no X_test given. Returning empty array.') + print(' Model = %s' %model) + return (pred_xgbs, model) + else: + if not isinstance(X_XGB_test, str): + print(' final predictions (may need to be transformed to original labels)', pred_xgbs[:10]) + if isinstance(pred_probas, list): + print(' predicted probabilities shape = [(%s, %s)...]' + %(pred_probas[0].shape[0],pred_probas[0].shape[1])) + else: + print(' predicted probabilities', pred_probas[:4]) + else: + print(' no X_test given. Returning empty array.') + print(' Model = %s' %model) + return (pred_xgbs, pred_probas, model) +################################################################################## +def complex_LightGBM_model(X_train, y_train, X_test, log_y=False, GPU_flag=False, + scaler = '', enc_method='label', n_splits=5, verbose=-1): + """ + This model is called complex because it handle multi-label, mulit-class datasets which LGBM ordinarily cant. + Just send in X_train, y_train and what you want to predict, X_test + It will automatically split X_train into multiple folds (10) and train and predict each time on X_test. + It will then use average (or use mode) to combine the results and give you a y_test. + It will automatically detect modeltype as "Regression" or 'Classification' + It will also add MultiOutputClassifier and MultiOutputRegressor to multi_label problems. + The underlying estimators in all cases is LGBM. So you get the best of both worlds. + + Inputs: + ------------ + X_train: pandas dataframe only: do not send in numpy arrays. This is the X_train of your dataset. + y_train: pandas Series or DataFrame only: do not send in numpy arrays. This is the y_train of your dataset. + X_test: pandas dataframe only: do not send in numpy arrays. This is the X_test of your dataset. + log_y: default = False: If True, it means use the log of the target variable "y" to train and test. + GPU_flag: if your machine has a GPU set this flag and it will use XGBoost GPU to speed up processing. + scaler : default is StandardScaler(). But you can send in MinMaxScaler() as input to change it or any other scaler. + enc_method: default is 'label' encoding. But you can choose 'glmm' as an alternative. But those are the only two. + verbose: default = 0. Choosing 1 will give you lot more output. + + Outputs: + ------------ + y_preds: Predicted values for your X_XGB_test dataframe. + It has been averaged after repeatedly predicting on X_XGB_test. So likely to be better than one model. + """ + X_XGB = copy.deepcopy(X_train) + Y_XGB = copy.deepcopy(y_train) + X_XGB_test = copy.deepcopy(X_test) + #################################### + start_time = time.time() + top_num = 10 + if isinstance(Y_XGB, pd.Series): + targets = [Y_XGB.name] + elif isinstance(Y_XGB, pd.DataFrame): + targets = Y_XGB.columns.tolist() + elif isinstance(Y_XGB, np.ndarray): + print(' y input is an numpy array and hence convert into a series or dataframe and re-try.') + return + else: + print('Dont use complex LightGBM models for single label problems. Try simple_LightGBM_model instead.') + return + if len(targets) == 1: + multi_label = False + if isinstance(Y_XGB, pd.DataFrame): + Y_XGB = pd.Series(Y_XGB.values.ravel(),name=targets[0], index=Y_XGB.index) + else: + multi_label = True + modeltype, _ = analyze_problem_type(Y_XGB, targets) + ### LightGBM ##### + if modeltype == 'Binary_Classification': + print('# LightGBM is not a good choice since XGBoost is better for binary classification problems.') + elif modeltype == 'Multi_Classification': + print('# LightGBM is a good choice since it is better for multi-class than XGBoost.') + else: + print('# LightGBM is a poor choice compared to XGBoost for Regression problems.') + + columns = X_XGB.columns + #### In some cases, there are special chars in column names. Remove them. ### + if np.array([':' in x for x in columns]).any(): + sel_preds = columns[np.array([':' in x for x in columns])].tolist() + print('removing special char : in %s since LightGBM does not like it...' %sel_preds) + columns = ["_".join(x.split(":")) for x in columns] + X_XGB.columns = columns + if not isinstance(X_XGB_test, str): + X_XGB_test.columns = columns + ################################################################################### + ######### S C A L E R P R O C E S S I N G B E G I N S ############ + ################################################################################### + if isinstance(scaler, str): + if not scaler == '': + scaler = scaler.lower() + if scaler == 'standard': + scaler = StandardScaler() + elif scaler == 'minmax': + scaler = MinMaxScaler() + else: + scaler = StandardScaler() + else: + scaler = StandardScaler() + else: + pass + ############################################################################# + ######### G P U P R O C E S S I N G B E G I N S ############# + ############################################################################# + if modeltype == 'Regression': + if log_y: + Y_XGB.loc[Y_XGB==0] = 1e-15 ### just set something that is zero to a very small number + + ######### Now set the number of rows we need to tune hyper params ### + scoreFunction = { "precision": "precision_weighted","recall": "recall_weighted"} + + #### We need a small validation data set for hyper-param tuning ############# + hyper_frac = 0.2 + #### now select a random sample from X_XGB ## + if modeltype == 'Regression': + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999) + else: + try: + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999, stratify = Y_XGB) + except: + ## In some small cases, you cannot stratify since there are too few samples. So leave it as is ## + X_train, X_valid, Y_train, Y_valid = train_test_split(X_XGB, Y_XGB, test_size=hyper_frac, + random_state=999) + + #### First convert test data into numeric using train data ### + X_train, Y_train, X_valid, Y_valid = data_transform(X_train, Y_train, X_valid, Y_valid, + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + ###### This step is needed for making sure y is transformed to log_y ###### + if modeltype == 'Regression' and log_y: + Y_train = np.log(Y_train) + Y_valid = np.log(Y_valid) + + random_search_flag = True + ###### Time to hyper-param tune model using randomizedsearchcv ######### + gbm_model = lightgbm_model_fit(random_search_flag, X_train, Y_train, X_valid, Y_valid, modeltype, + multi_label, log_y, model="") + if multi_label: + model = copy.deepcopy(gbm_model) + else: + model = gbm_model.best_estimator_ + #### First convert test data into numeric using train data ### + if not isinstance(X_XGB_test, str): + x_train, y_train, x_test, _ = data_transform(X_XGB, Y_XGB, X_XGB_test, "", + modeltype, multi_label, scaler=scaler, enc_method=enc_method) + + ###### Time to train the hyper-tuned model on full train data ######### + random_search_flag = False + model = lightgbm_model_fit(random_search_flag, x_train, y_train, x_test, "", modeltype, + multi_label, log_y, model=model) + ############# Time to get feature importances based on full train data ################ + if multi_label: + for i,target_name in enumerate(targets): + print('Top 10 features for {}: {}'.format(target_name,pd.DataFrame(model.estimators_[i].feature_importances_, + index=model.estimators_[i].feature_name_, + columns=['importance']).sort_values('importance', ascending=False).index.tolist()[:10])) + else: + print('Top 10 features:\n', pd.DataFrame( + model.booster_.feature_importance(importance_type='gain'),index=columns, + columns=['importance']).sort_values('importance', ascending=False).index.tolist()[:10]) + ###### Time to consolidate the predictions on test data ######### + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + if log_y: + pred_xgbs = np.exp(model.predict(x_test)) + else: + pred_xgbs = model.predict(x_test) + #### if there is no test data just return empty strings ### + else: + pred_xgbs = [] + else: + if not isinstance(X_XGB_test, str): + if not multi_label: + pred_xgbs = model.predict(x_test) + pred_probas = model.predict_proba(x_test) + else: + ### This is how you have to process if it is multi_label ## + pred_probas = model.predict_proba(x_test) + predsy = [np.argmax(line,axis=1) for line in pred_probas] + pred_xgbs = np.array(predsy) + pred_xgbs = pred_xgbs.reshape(-1, len(predsy)) + else: + pred_xgbs = [] + pred_probas = [] + ##### once the entire model is trained on full train data ################## + print(' Time taken for training Light GBM on entire train data (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + if multi_label: + for i,target_name in enumerate(targets): + lgbm.plot_importance(model.estimators_[i], importance_type='gain', max_num_features=top_num, + title='LGBM model feature importances for %s' %target_name) + else: + lgbm.plot_importance(model, importance_type='gain', max_num_features=top_num, + title='LGBM final model feature importances') + print('Returning the following:') + print(' Model = %s' %model) + if modeltype == 'Regression': + if not isinstance(X_XGB_test, str): + print(' final predictions', pred_xgbs[:10]) + return (pred_xgbs, model) + else: + if not isinstance(X_XGB_test, str): + print(' final predictions (may need to be transformed to original labels)', pred_xgbs[:10]) + if isinstance(pred_probas, list): + print(' predicted probabilities shape = [(%s, %s)...]' + %(pred_probas[0].shape[0],pred_probas[0].shape[1])) + else: + print(' predicted probabilities', pred_probas[:4]) + return (pred_xgbs, pred_probas, model) +############################################################################################# +import copy +import time +from sklearn.model_selection import RepeatedStratifiedKFold, RepeatedKFold +from sklearn.metrics import r2_score, mean_squared_error +from sklearn.metrics import roc_auc_score, f1_score +import matplotlib.pyplot as plt +import seaborn as sns +from scipy import stats +from sklearn.model_selection import RandomizedSearchCV +import scipy as sp +from sklearn.preprocessing import MinMaxScaler +import pdb +def simple_LightGBM_model(X_train, y_train, X_test, log_y=False, + GPU_flag=False, scaler='', enc_method='label', n_splits=5, verbose=-1): + """ + This is a simple lightGBM model that works only on single label problems. + """ + from tqdm import tqdm + X_index = X_train.index + if isinstance(y_train, np.ndarray): + y_train = pd.Series(y_train, index=X_index) + num_splits = 5 + num_repeats = 5 + if X_train.shape[0] <= 100000: + n_estimators = 1000 # LightGBM is fast but mak sure it is small + early_stopping_rounds = 100 + else: + n_estimators = 500 # make sure it is small + early_stopping_rounds = 10 + ###### Now you can set these defaults ## + verbose = False + SEED = 42 + X_XGB = copy.deepcopy(X_train) + Y_XGB = copy.deepcopy(y_train) + X_XGB_test = copy.deepcopy(X_test) + #################################### + start_time = time.time() + top_num = 10 + num_boost_round = 400 + if isinstance(Y_XGB, pd.Series): + targets = [Y_XGB.name] + elif isinstance(Y_XGB, np.ndarray): + print(' y input is an numpy array and hence convert into a series or dataframe and re-try.') + return + else: + targets = Y_XGB.columns.tolist() + if len(targets) == 1: + multi_label = False + if isinstance(Y_XGB, pd.DataFrame): + Y_XGB = pd.Series(Y_XGB.values.ravel(),name=targets[0], index=Y_XGB.index) + else: + multi_label = True + modeltype, multi_label = analyze_problem_type(Y_XGB, targets) + print('* LightGBM model training started... *') + if multi_label: + model_label = 'Multi_Label' + print('This is a %s problem. You must use complex_LightGBM_model for this dataset.' %model_label) + return + else: + model_label = 'Single_Label' + columns = X_XGB.columns + ############################################## + rand_params = { + #'learning_rate': sp.stats.uniform(scale=1), + #'learning_rate': np.linspace(1e-8,1e-1), + #'num_leaves': sp.stats.randint(2, 30), + 'n_estimators': sp.stats.randint(100,400), + #"max_depth": sp.stats.randint(2, 7), + #'subsample': sp.stats.uniform(scale=1), + #'colsample_bytree': sp.stats.uniform(scale=1), + #'class_weight':[None, 'balanced'] + } + + gpu_exists = check_if_GPU_exists(verbose=1) + if modeltype == 'Regression': + if gpu_exists: + lgbmx = lgbm.LGBMRegressor(device="gpu") + else: + lgbmx = lgbm.LGBMRegressor() + objective = 'regression' + metric = 'rmse' + is_unbalance = False + class_weight = None + score_name = 'Score' + else: + if modeltype =='Binary_Classification': + if gpu_exists: + lgbmx = lgbm.LGBMClassifier(device="gpu") + else: + lgbmx = lgbm.LGBMClassifier() + objective = 'binary' + metric = 'auc' + is_unbalance = True + class_weight = None + score_name = 'ROC AUC' + num_class = 1 + else: + if gpu_exists: + lgbmx = lgbm.LGBMClassifier(device="gpu") + else: + lgbmx = lgbm.LGBMClassifier() + objective = 'multiclass' + #objective = 'multiclassova' + metric = 'multi_logloss' + is_unbalance = True + class_weight = 'balanced' + score_name = 'Multiclass Logloss' + if multi_label: + if isinstance(y_train, np.ndarray): + num_class = np.unique(y_train).max() + 1 + else: + num_class = y_train.nunique().max() + else: + if isinstance(y_train, np.ndarray): + num_class = np.unique(y_train).max() + 1 + else: + num_class = y_train.nunique() + + if modeltype == 'Regression': + ## there is no num_class in regression for LGBM model ## + lgbm_params = { + 'objective': objective, + 'metric': metric, + 'boosting_type': 'gbdt', + 'save_binary': True, + 'seed': 1337, 'feature_fraction_seed': 1337, + 'bagging_seed': 1337, 'drop_seed': 1337, + 'data_random_seed': 1337, + 'verbose': -1, + 'n_estimators': n_estimators, + } + else: + lgbm_params = { + 'objective': objective, + 'metric': metric, + 'boosting_type': 'gbdt', + 'save_binary': True, + 'seed': 1337, 'feature_fraction_seed': 1337, + 'bagging_seed': 1337, 'drop_seed': 1337, + 'data_random_seed': 1337, + 'verbose': -1, + 'num_class': num_class, + 'is_unbalance': is_unbalance, + 'class_weight': class_weight, + 'n_estimators': n_estimators, + } + + lgbm_copy = copy.deepcopy(lgbmx) + lgb_importances = pd.DataFrame() + lgb_oof = np.zeros(X_train.shape[0]) + lgb_pred = np.zeros(X_test.shape[0]) + + start = time.time() + if modeltype == 'Regression': + scoring = 'neg_mean_squared_error' + score_name = 'MSE' + else: + if modeltype =='Binary_Classification': + scoring = 'roc_auc' + score_name = 'ROC AUC' + else: + scoring = 'balanced_accuracy' + score_name = 'balanced_accuracy' + print('Starting Hyper Param tuning of %s lightGBM model. This will take time...' %model_label) + lgbmx.set_params(**lgbm_params) + + model = RandomizedSearchCV(lgbmx, + param_distributions = rand_params, + n_iter = 5, + return_train_score = True, + random_state = 99, + n_jobs=-1, + cv = 5, + refit=True, + scoring = scoring, + verbose = False) + model.fit(X_train, y_train) + print('Time taken for Hyper Param tuning of LightGBM model (in minutes) = %0.1f' %( + (time.time()-start_time)/60)) + cv_results = pd.DataFrame(model.cv_results_) + if modeltype == 'Regression': + print(' Mean cross-validated train %s score = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_train_score'].mean())))) + print(' Mean cross-validated test %s score = %0.04f' %(score_name, np.sqrt(abs(cv_results['mean_test_score'].mean())))) + else: + print(' Mean cross-validated train score %s = %0.04f' %(score_name, cv_results['mean_train_score'].mean())) + print(' Mean cross-validated test score %s = %0.04f' %(score_name, cv_results['mean_test_score'].mean())) + ### In this case, there is no boost rounds so just return the default num_boost_round + best_params = model.best_params_ + print(' Hyper tuned params are: %s' %best_params) + start_time = time.time() + i = 0 + scores = [] + if modeltype == 'Regression': + repeat_strats = RepeatedKFold(n_splits=num_splits, n_repeats=num_repeats, random_state=SEED) + else: + repeat_strats = RepeatedStratifiedKFold(n_splits=num_splits, n_repeats=num_repeats, random_state=SEED) + #### This is where we repeatedly make training and predictions ###################### + for fold, (trn_idx, val_idx) in enumerate(repeat_strats.split(X=X_train, y=y_train)): + start_time = time.time() + xx_train, yy_train = X_train.iloc[trn_idx], y_train.iloc[trn_idx] + xx_valid, yy_valid = X_train.iloc[val_idx], y_train.iloc[val_idx] + + es = lgbm.early_stopping( + stopping_rounds=early_stopping_rounds, + first_metric_only=True, + verbose=verbose, + ) + + #le = lgbm.log_evaluation( + # period=verbose, + # show_stdv=verbose + #) + ########## Now train the model ########### + lgbm_copy.set_params(**lgbm_params) + lgbm_copy.set_params(**best_params) + model = copy.deepcopy(lgbm_copy) + if i == 0: + print(' Training hyper-tuned', model) + i += 1 + + model.fit( + xx_train, + yy_train, + eval_set=[(xx_valid, yy_valid)], + eval_names=['train', 'valid'], + eval_metric=metric, + callbacks=[es], + ) + + fi_tmp = pd.DataFrame() + fi_tmp['feature'] = xx_train.columns.tolist() + fi_tmp['importance'] = model.feature_importances_ + fi_tmp['fold'] = fold + fi_tmp['seed'] = SEED + lgb_importances = lgb_importances.append(fi_tmp) + + lgb_oof[val_idx] = model.predict(xx_valid) + lgb_pred += model.predict(X_test) / num_splits / num_repeats + + if modeltype == 'Regression': + auc = np.sqrt(mean_squared_error(yy_valid, lgb_oof[val_idx])) + scores.append(auc) + print(f" iteration {i}: RMSE: {auc:.2f}") + else: + if modeltype =='Binary_Classification': + auc = roc_auc_score(yy_valid, lgb_oof[val_idx]) + scores.append(auc) + print(f" iteration {i}: ROC AUC: {auc:.2f}") + else: + auc = f1_score(yy_valid, lgb_oof[val_idx], average="macro") + #auc = roc_auc_score(yy_valid, lgb_oof[val_idx], multi_class='ovr',average="macro") + scores.append(auc) + print(f" iteration {i}: Macro F1 score: {auc:.2f}") + + elapsed = time.time() - start_time + + if modeltype == 'Regression': + auc = np.sqrt(mean_squared_error(y_train, lgb_oof)) + print(f"Average Train RMSE: {auc:6f}, elapsed time: {elapsed:.0f} seconds") + else: + if modeltype =='Binary_Classification': + auc = roc_auc_score(y_train, lgb_oof) + print(f"Average Train AUC: {auc:6f}, elapsed time: {elapsed:.0f} seconds") + else: + auc = f1_score(y_train, lgb_oof, average="macro") + print(f"Average Train Macro F1 score: {auc:6f}, elapsed time: {elapsed:.0f} seconds") + + #### Now change the probas to fit within 0 and 1 ####### + MM = MinMaxScaler() + print('Fitting model on entire train dataset...') + start_time = time.time() + model = copy.deepcopy(lgbm_copy) + model.fit( + X_train, + y_train, + ) + elapsed = time.time() - start_time + print(f" Training time: {elapsed:.0f} seconds") + order = list(lgb_importances.groupby("feature").mean().sort_values("importance", ascending=False).index) + plt.figure(figsize=(12, 4), tight_layout=True) + sns.barplot(x="importance", y="feature", data=lgb_importances, order=order[:15]) + plt.title("{} feature importances".format("lgb")) + plt.tight_layout() + if modeltype == 'Regression': + lgb_preds = lgb_pred.ravel() + lgb_probas = np.array([]) + else: + lgb_probas = MM.fit_transform(lgb_pred.reshape(-1, 1)) + lgb_preds = (lgb_probas>0.5).astype(int) + print('Returning the following:') + print(' final predictions sample', lgb_preds[:4]) + print(' predicted probabilities sample', lgb_probas[:4]) + print(' Model = %s' %model) + return lgb_preds, lgb_probas, model +######################################################################################## +def plot_importances_XGB(train_set, labels, ls, y_preds, modeltype, top_num='all'): + add_items=0 + for item in ls: + add_items +=item + + if isinstance(top_num, str): + feat_imp=pd.DataFrame(add_items/len(ls),index=train_set.columns, + columns=["importance"]).sort_values('importance', ascending=False) + feat_imp2=feat_imp[feat_imp>0.00005] + #df_cv=df_cv.reset_index() + #### don't add [:top_num] at the end of this statement since it will error ####### + #feat_imp = pd.Series(df_cv.importance.values, + # index=df_cv.drop(["importance"], axis=1)).sort_values(axis='index',ascending=False) + else: + ## this limits the number of items to the top_num items + feat_imp=pd.DataFrame(add_items/len(ls),index=train_set.columns[:top_num], + columns=["importance"]).sort_values('importance', ascending=False) + feat_imp2=feat_imp[feat_imp>0.00005] + #df_cv=df_cv.reset_index() + #feat_imp = pd.Series(df_cv.importance.values, + # index=df_cv.drop(["importance"], axis=1)).sort_values(axis='index',ascending=False)[:top_num] + ##### Now plot the feature importances ################# + imp_columns=[] + for item in pd.DataFrame(feat_imp2).reset_index()["index"].tolist(): + fcols=re.sub("[(),]","",str(item)) + try: + columns= int(re.sub("['']","",fcols)) + imp_columns.append(columns) + except: + columns= re.sub("['']","",fcols) + imp_columns.append(columns) + # X_UPDATED=X_GB[imp_columns] + len(imp_columns) + fig = plt.figure(figsize=(15,8)) + ax1=plt.subplot(2, 2, 1) + if isinstance(top_num, str): + feat_imp2[:].plot(kind='barh', ax=ax1, title='Feature importances of model on test data') + else: + feat_imp2[:top_num].plot(kind='barh', ax=ax1, title='Feature importances of model on test data') + if modeltype == 'Regression': + ax2=plt.subplot(2, 2, 2) + pd.Series(y_preds).plot(ax=ax2, color='b', title='Model predictions on test data'); + else: + ax2=plt.subplot(2, 2, 2) + pd.Series(y_preds).hist(ax=ax2, color='b', label='Model predictions histogram on test data'); +################################################################################## +def analyze_problem_type(y_train, target, verbose=0) : + y_train = copy.deepcopy(y_train) + cat_limit = 30 ### this determines the number of categories to name integers as classification ## + float_limit = 15 ### this limits the number of float variable categories for it to become cat var + if isinstance(target, str): + multi_label = False + string_target = True + else: + if len(target) == 1: + multi_label = False + string_target = False + else: + multi_label = True + string_target = False + + #### This is where you detect what kind of problem it is ################# + if string_target or type(y_train) == pd.Series: + ## If target is a string then we should test for dtypes this way ##### + if y_train.dtype in ['int64', 'int32','int16']: + if len(np.unique(y_train)) <= 2: + model_class = 'Binary_Classification' + elif len(y_train.unique()) > 2 and len(y_train.unique()) <= cat_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + elif y_train.dtype in ['float16','float32','float64']: + if len(y_train.unique()) <= 2: + model_class = 'Binary_Classification' + elif len(y_train.unique()) > 2 and len(y_train.unique()) <= float_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + else: + if len(y_train.unique()) <= 2: + model_class = 'Binary_Classification' + else: + model_class = 'Multi_Classification' + else: + for i in range(y_train.shape[1]): + ### if target is a list, then we should test dtypes a different way ### + if y_train.dtypes.values.all() in ['int64', 'int32','int16']: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + elif len(np.unique(y_train.iloc[:,0])) > 2 and len(np.unique(y_train.iloc[:,0])) <= cat_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + elif y_train.dtypes.values.all() in ['float16','float32','float64']: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + elif len(np.unique(y_train.iloc[:,0])) > 2 and len(np.unique(y_train.iloc[:,0])) <= float_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + else: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + else: + model_class = 'Multi_Classification' + ########### print this for the start of next step ########### + if multi_label: + print(''' %s %s problem ''' %('Multi_Label', model_class)) + else: + print(''' %s %s problem ''' %('Single_Label', model_class)) + return model_class, multi_label +############################################################################### \ No newline at end of file diff --git a/build/lib/featurewiz/my_encoders.py b/build/lib/featurewiz/my_encoders.py new file mode 100644 index 0000000..803bef5 --- /dev/null +++ b/build/lib/featurewiz/my_encoders.py @@ -0,0 +1,2145 @@ +import numpy as np +import pandas as pd +from sklearn.impute import SimpleImputer +#from sklearn.preprocessing import OneHotEncoder +from sklearn.feature_extraction.text import CountVectorizer +from sklearn.linear_model import LogisticRegression +from sklearn.compose import make_column_transformer +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import LabelEncoder, LabelBinarizer +from sklearn.base import BaseEstimator, TransformerMixin #gives fit_transform method for free +import pdb +import copy +from sklearn.base import TransformerMixin +from collections import defaultdict +from category_encoders import OneHotEncoder +from category_encoders import HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder +from category_encoders import HelmertEncoder, OrdinalEncoder, CountEncoder, BaseNEncoder +from category_encoders import TargetEncoder, CatBoostEncoder, WOEEncoder, JamesSteinEncoder +#from sklearn.preprocessing import OneHotEncoder +from category_encoders.glmm import GLMMEncoder +from sklearn.preprocessing import LabelEncoder +from category_encoders.wrapper import PolynomialWrapper +from sklearn.preprocessing import FunctionTransformer +from pandas.api.types import is_datetime64_any_dtype + +################################################################################# +def left_subtract(l1,l2): + lst = [] + for i in l1: + if i not in l2: + lst.append(i) + return lst +################################################################################# +class My_LabelEncoder(BaseEstimator, TransformerMixin): + """ + ################################################################################################ + ###### The My_LabelEncoder class works just like sklearn's Label Encoder but better! ####### + ##### It label encodes any cat var in your dataset. It also handles NaN's in your dataset! #### + ## The beauty of this function is that it takes care of NaN's and unknown (future) values.##### + ##################### This is the BEST working version - don't mess with it!! ################## + ################################################################################################ + Usage: + le = My_LabelEncoder() + le.fit_transform(train[column]) ## this will give your transformed values as an array + le.transform(test[column]) ### this will give your transformed values as an array + + Usage in Column Transformers and Pipelines: + No. It cannot be used in pipelines since it need to produce two columns for the next stage in pipeline. + See my other module called My_LabelEncoder_Pipe() to see how it can be used in Pipelines. + """ + def __init__(self): + self.transformer = defaultdict(str) + self.inverse_transformer = defaultdict(str) + self.max_val = 0 + + def fit(self,testx, y=None): + ### Do not change this since Rare class combiner requires this test ## + if isinstance(testx, tuple): + testx = testx[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(testx, pd.Series): + pass + elif isinstance(testx, np.ndarray): + testx = pd.Series(testx) + else: + #### There is no way to transform dataframes since you will get a nested renamer error if you try ### + ### But if it is a one-dimensional dataframe, convert it into a Series + if testx.shape[1] == 1: + testx = pd.Series(testx.values.ravel(),name=testx.columns[0]) + else: + #### Since it is multi-dimensional, So in this case, just return the data as is + return self + ins = np.unique(testx.factorize()[1]).tolist() + outs = np.unique(testx.factorize()[0]).tolist() + #ins = testx.value_counts(dropna=False).index + if -1 in outs: + # it already has nan if -1 is in outs. No need to add it. + if not np.nan in ins: + ins.insert(0,np.nan) + self.transformer = dict(zip(ins,outs)) + self.inverse_transformer = dict(zip(outs,ins)) + return self + + def transform(self, testx, y=None): + ### Do not change this since Rare class combiner requires this test ## + if isinstance(testx, tuple): + y = testx[1] + testx = testx[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(testx, pd.Series): + pass + elif isinstance(testx, np.ndarray): + testx = pd.Series(testx) + else: + #### There is no way to transform dataframes since you will get a nested renamer error if you try ### + ### But if it is a one-dimensional dataframe, convert it into a Series + if testx.shape[1] == 1: + testx = pd.Series(testx.values.ravel(),name=testx.columns[0]) + else: + #### Since it is multi-dimensional, So in this case, just return the data as is + return testx, y + ### now convert the input to transformer dictionary values + new_ins = np.unique(testx.factorize()[1]).tolist() + missing = [x for x in new_ins if x not in self.transformer.keys()] + if len(missing) > 0: + for each_missing in missing: + self.transformer[each_missing] = int(self.max_val + 1) + self.inverse_transformer[int(self.max_val+1)] = each_missing + self.max_val = int(self.max_val+1) + else: + self.max_val = np.max(list(self.transformer.values())) + outs = testx.map(self.transformer).values.astype(int) + ### To handle category dtype you must do the next step ##### + testk = testx.map(self.transformer) ## this must be still a pd.Series + if testx.dtype not in [np.int16, np.int32, np.int64,np.int8, float, bool, object]: + if testx.isnull().sum().sum() > 0: + fillval = self.transformer[np.nan] + testk = testk.cat.add_categories([fillval]) + testk = testk.fillna(fillval) + testk = testk.astype(int) + return testk, y + else: + testk = testk.astype(int) + return testk, y + else: + return outs + + def inverse_transform(self, testx, y=None): + ### now convert the input to transformer dictionary values + if isinstance(testx, pd.Series): + outs = testx.map(self.inverse_transformer).values + elif isinstance(testx, np.ndarray): + outs = pd.Series(testx).map(self.inverse_transformer).values + else: + outs = testx[:] + return outs +################################################################################# +from collections import defaultdict +# This is needed to make this a regular transformer ### +from sklearn.base import BaseEstimator, TransformerMixin +class Rare_Class_Combiner_Pipe(BaseEstimator, TransformerMixin ): + """ + This is the pipeline version of rare class combiner used in sklearn pipelines. + """ + def __init__(self, transformers={} ): + # store the number of dimension of the target to predict an array of + # similar shape at predict + self.transformers = transformers + self.zero_low_counts = defaultdict(bool) + + def get_params(self, deep=True): + # This is to make it scikit-learn compatible #### + return {"transformers": self.transformers} + + def set_params(self, **parameters): + for parameter, value in parameters.items(): + setattr(self, parameter, value) + return self + + def fit(self, X, y=None, **fit_params): + """Fit the model according to the given training data""" + X = copy.deepcopy(X) + # transformers need a default name for rare categories ## + def return_cat_value(): + return "rare_categories" + ### In this case X itself will only be a pd.Series ### + each_catvar = X.name + #### if it is already a list, then leave it as is ### + self.transformers[each_catvar] = defaultdict(return_cat_value) + ### Then find the unique categories in the column ### + self.transformers[each_catvar] = dict(zip(X.unique(), X.unique())) + low_counts = pd.DataFrame(X).apply(lambda x: x.value_counts()[ + (x.value_counts()<=(0.01*x.shape[0])).values].index).values.ravel() + + if len(low_counts) == 0: + self.zero_low_counts[each_catvar] = True + else: + self.zero_low_counts[each_catvar] = False + for each_low in low_counts: + self.transformers[each_catvar].update({each_low:'rare_categories'}) + return self + + def transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + each_catvar = X.name + if self.zero_low_counts[each_catvar]: + pass + else: + X = X.map(self.transformers[each_catvar]) + ### simply fill in the missing values with the word "missing" ## + X = X.fillna('missing') + return X + + def fit_transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + ### Since X for yT in a pipeline is sent as X, we need to switch X and y this way ## + self.fit(X, y) + each_catvar = X.name + if self.zero_low_counts[each_catvar]: + pass + else: + X = X.map(self.transformers[each_catvar]) + ### simply fill in the missing values with the word "missing" ## + X = X.fillna('missing') + return X + + def inverse_transform(self, X, **fit_params): + ### One problem with this approach is that you have combined categories into one. + ### You cannot uncombine them since they no longer have a unique category. + ### You will get back the last transformed category when you inverse transform it. + each_catvar = X.name + transformer_ = self.transformers[each_catvar] + reverse_transformer_ = {y: x for (x, y) in transformer_.items()} + if self.zero_low_counts[each_catvar]: + pass + else: + X[each_catvar] = X[each_catvar].map(reverse_transformer_).values + return X + + def predict(self, X, y=None, **fit_params): + #print('There is no predict function in Rare class combiner. Returning...') + return X +###################################################################################### +from pandas.api.types import is_numeric_dtype +class Rare_Class_Combiner(BaseEstimator, TransformerMixin): + """ + This is the general version of combining classes in categorical vars. + You cannot use it in sklearn pipelines. You can however use it alone to make changes. + """ + def __init__(self, transformers={}, categorical_features=[], zero_low_counts=False): + # store the number of dimension of the target to predict an array of + # similar shape at predict + self.transformers = transformers + self.categorical_features = categorical_features + self.zero_low_counts = {} + if zero_low_counts: + for each_cat in categorical_features: + self.zero_low_counts[each_cat] = zero_low_counts + else: + for each_cat in categorical_features: + self.zero_low_counts[each_cat] = 0 + + + def get_params(self, deep=True): + # This is to make it scikit-learn compatible #### + return {"transformers": self.transformers, "categorical_features": self.categorical_features, + "zero_low_counts": self.zero_low_counts} + + def set_params(self, **parameters): + for parameter, value in parameters.items(): + setattr(self, parameter, value) + return self + + def fit(self, X, y=None, **fit_params): + """Fit the model according to the given training data""" + X = copy.deepcopy(X) + # transformers need a default name for rare categories ## + # transformers are designed to modify X which is 2d dimensional + if len(self.categorical_features) == 0: + if isinstance(X, pd.Series): + self.categorical_features = [X.name] + elif isinstance(X, np.ndarray): + print('Error: Input cannot be a numpy array for transformers') + return X, y + else: + # if X is a dataframe, then you need the list of features ## + self.categorical_features = X.columns.tolist() + if isinstance(self.categorical_features, str): + self.categorical_features = [self.categorical_features] + #### if it is already a list, then leave it as is ### + for i, each_catvar in enumerate(self.categorical_features): + if is_numeric_dtype(X[each_catvar]): + max_value = X[each_catvar].max() + save_value = max_value+1 + else: + save_value = "rare_categories" + ### Then find the unique categories in the column ### + self.transformers[each_catvar] = dict(zip(X[each_catvar].unique(),X[each_catvar].unique())) + low_counts = X[[each_catvar]].apply(lambda x: x.value_counts()[ + (x.value_counts()<=(0.01*x.shape[0])).values].index).values.ravel() + ### This is where we find whether cat var has even a single low category ### + if len(low_counts) == 0: + self.zero_low_counts[each_catvar] = save_value + else: + self.zero_low_counts[each_catvar] = 0 + for each_low in low_counts: + self.transformers[each_catvar].update({each_low: save_value}) + return self + + def transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + for i, each_catvar in enumerate(self.categorical_features): + if self.zero_low_counts[each_catvar]: + continue + else: + X[each_catvar] = X[each_catvar].map(self.transformers[each_catvar]).values + ### simply fill in the missing values with the word "missing" ## + ### Remember that fillna only works at dataframe level! ## + X[[each_catvar]] = X[[each_catvar]].fillna('missing') + return X + + def fit_transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + ### Since X for yT in a pipeline is sent as X, we need to switch X and y this way ## + self.fit(X, y) + for i, each_catvar in enumerate(self.categorical_features): + if self.zero_low_counts[each_catvar]: + continue + else: + X[each_catvar] = X[each_catvar].map(self.transformers[each_catvar]).values + ### simply fill in the missing values with the word "missing" ## + ### Remember that fillna only works at dataframe level! ## + X[[each_catvar]] = X[[each_catvar]].fillna('missing') + return X + + def inverse_transform(self, X, **fit_params): + ### One problem with this approach is that you have combined categories into one. + ### You cannot uncombine them since they no longer have a unique category. + ### You will get back the last transformed category when you inverse transform it. + for i, each_catvar in enumerate(self.categorical_features): + transformer_ = self.transformers[each_catvar] + reverse_transformer_ = dict([(y,x) for (x,y) in transformer_.items()]) + if self.zero_low_counts[each_catvar]: + continue + else: + X[each_catvar] = X[each_catvar].map(reverse_transformer_).values + return X + + def predict(self, X, y=None, **fit_params): + #print('There is no predict function in Rare class combiner. Returning...') + return X +###################################################################################### +class My_LabelEncoder_Pipe(BaseEstimator, TransformerMixin): + """ + ################################################################################################ + ###### The My_LabelEncoder_Pipe class works just like sklearn's Label Encoder but better! ##### + ##### It label encodes any cat var in your dataset. But it can also be used in Pipelines! ##### + ## The beauty of this function is that it takes care of NaN's and unknown (future) values.##### + ##### Since it produces an unused second column it can be used in sklearn's Pipelines. ##### + ##### But for that you need to add a drop_second_col() function to this My_LabelEncoder_Pipe ## + ##### and then feed the whole pipeline to a Column_Transformer function. It is very easy. ##### + ##################### This is the BEST working version - don't mess with it!! ################## + ################################################################################################ + Usage in pipelines: + le = My_LabelEncoder_Pipe() + le.fit_transform(train[column]) ## this will give you two columns - beware! + le.transform(test[column]) ### this will give you two columns - beware! + + Usage in Column Transformers: + def drop_second_col(Xt): + ### This deletes the 2nd column. Hence col number=1 and axis=1 ### + return np.delete(Xt, 1, 1) + + drop_second_col_func = FunctionTransformer(drop_second_col) + + le_one = make_pipeline(le, drop_second_col_func) + + ct = make_column_transformer( + (le_one, catvars[0]), + (le_one, catvars[1]), + (imp, numvars), + remainder=remainder) + + """ + def __init__(self): + self.transformer = defaultdict(str) + self.inverse_transformer = defaultdict(str) + self.max_val = 0 + + def fit(self,testx, y=None): + ### Do not change this since Rare class combiner requires this test ## + if isinstance(testx, tuple): + y = testx[1] + testx = testx[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(testx, pd.Series): + pass + elif isinstance(testx, np.ndarray): + testx = pd.Series(testx) + else: + #### There is no way to transform dataframes since you will get a nested renamer error if you try ### + ### But if it is a one-dimensional dataframe, convert it into a Series + + if testx.shape[1] == 1: + testx = pd.Series(testx.values.ravel(),name=testx.columns[0]) + else: + #### Since it is multi-dimensional, So in this case, just return the data as is + return self + ins = np.unique(testx.factorize()[1]).tolist() + outs = np.unique(testx.factorize()[0]).tolist() + #ins = testx.value_counts(dropna=False).index + if -1 in outs: + # it already has nan if -1 is in outs. No need to add it. + if not np.nan in ins: + ins.insert(0,np.nan) + self.transformer = dict(zip(ins,outs)) + self.inverse_transformer = dict(zip(outs,ins)) + return self + + def transform(self, testx, y=None): + ### Do not change this since Rare class combiner requires this test ## + if isinstance(testx, tuple): + y = testx[1] + testx = testx[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(testx, pd.Series): + pass + elif isinstance(testx, np.ndarray): + testx = pd.Series(testx) + else: + #### There is no way to transform dataframes since you will get a nested renamer error if you try ### + ### But if it is a one-dimensional dataframe, convert it into a Series + if testx.shape[1] == 1: + testx = pd.Series(testx.values.ravel(),name=testx.columns[0]) + else: + #### Since it is multi-dimensional, So in this case, just return the data as is + return testx, y + ### now convert the input to transformer dictionary values + new_ins = np.unique(testx.factorize()[1]).tolist() + missing = [x for x in new_ins if x not in self.transformer.keys()] + if len(missing) > 0: + for each_missing in missing: + self.transformer[each_missing] = int(self.max_val + 1) + self.inverse_transformer[int(self.max_val+1)] = each_missing + self.max_val = int(self.max_val+1) + else: + self.max_val = np.max(list(self.transformer.values())) + outs = testx.map(self.transformer).values + testk = testx.map(self.transformer) + if testx.dtype not in [np.int16, np.int32, np.int64, float, bool, object]: + if testx.isnull().sum().sum() > 0: + fillval = self.transformer[np.nan] + testk = testk.cat.add_categories([fillval]) + testk = testk.fillna(fillval) + testk = testk.astype(int) + return testk, y + else: + testk = testk.astype(int) + return testk, y + else: + return np.c_[outs,np.zeros(shape=outs.shape)].astype(int) + + def inverse_transform(self, testx, y=None): + ### now convert the input to transformer dictionary values + if isinstance(testx, pd.Series): + outs = testx.map(self.inverse_transformer).values + elif isinstance(testx, np.ndarray): + outs = pd.Series(testx).map(self.inverse_transformer).values + else: + outs = testx[:] + return outs +################################################################################# +class Groupby_Aggregator(BaseEstimator, TransformerMixin): + """ + ################################################################################################# + ###### This Groupby_Aggregator Class works just like any Transformer in sklearn ############### + ##### You can add any groupby features based on categorical columns in a data frame ########### + ### The list of numeric features grouped by each categorical must be given as input ############ + ### You cannot use it in sklearn pipelines but can use it stand-alone to create features. ####### + ##### It uses the same fit() and fit_transform() methods of sklearn's LabelEncoder class. ##### + ################################################################################################# + ### This function is a very fast function that will iteratively compute aggregates for numerics + ### It returns original dataframe with added features using numeric variables aggregated + ### What are aggregate? aggregates can be "count, "mean", "median", "mode", "min", "max", etc. + ### What do we aggregrate? all numeric columns in your data + ### What do we groupby? categorical columns which are usually object or string varaiables. + ### Make sure to select best features afterwards using FE_remove_variables_using_SULOV_method. + ################################################################################################# + ### Inputs: + ### dft: Just sent in the data frame df that you want features added to + ### agg_types: list of computational types: 'mean','median','count', 'max', 'min', 'sum', etc. + ### One caveat: these agg_types must be found in the following agg_func of numpy + ### or pandas groupby statements. + ### List of aggregates available: {'count','sum','mean','mad','median','min','max', + ### 'mode','abs', 'prod','std','var','sem','skew','kurt', + ### 'quantile','cumsum','cumprod','cummax','cummin'} + ### categoricals: columns to groupby all the numeric features and compute aggregates by. + ### numerics: columns that will be grouped by categoricals above using aggregate types. + ### Outputs: + ### dataframe: The same input dataframe with additional features created by this function. + ################################################################################################# + Usage: + MGB = Groupby_Aggregator(categoricals=catcols,aggregates=['mean','skew'], numerics=numerics) + X_train = MGB.fit_transform(X_train) + X_test = MGB.transform(X_test) + """ + def __init__(self, categoricals=[], aggregates=[], numerics='all'): + # store the number of dimension of the target to predict an array of + # similar shape at predict + self.transformers = defaultdict(str) + self.categoricals = categoricals + self.agg_types = aggregates + self.numerics = numerics + self.train_cols = defaultdict(str) + self.func_set = {'count','sum','mean','mad','median','min','max','mode', + 'std','var','sem', 'skew','kurt','abs', 'prod', + 'quantile','cumsum','cumprod','cummax','cummin'} + + def get_params(self, deep=True): + # This is to make it scikit-learn compatible #### + return {"categoricals": self.categoricals, "aggregates": self.agg_types, + "numerics": self.numerics} + + def set_params(self, **parameters): + for parameter, value in parameters.items(): + setattr(self, parameter, value) + return self + + def fit(self, X, **fit_params): + """Fit the model according to the given training data""" + ##### First make a copy of dataframe ### + dft_index = X.index + dft = copy.deepcopy(X) + # transformers are designed to modify X which must be multi-dimensional + if isinstance(X, pd.Series) or isinstance(X, np.ndarray): + print('Data cannot be a numpy array or a pandas Series. Must be dataframe!') + return X + if isinstance(self.categoricals, str): + self.categoricals = [self.categoricals] + if isinstance(self.numerics, str): + if self.numerics != 'all': + self.numerics = [self.numerics] + ### Make sure the list of functions they send in are acceptable functions ## + ls = X.select_dtypes('number').columns.tolist() + if self.numerics == 'all': + self.numerics = copy.deepcopy(ls) + ### Make sure that the numerics are numeric variables! ## + try: + print('Beware: Potentially creates %d features (some will be dropped due to zero variance)' %( + len(self.numerics)*len(self.categoricals)*len(self.agg_types))) + except Exception as e: + print('Erroring due to %s' %e) + #self.numerics = list(set(self.numerics).intersection(ls)) + ### Make sure that the aggregate functions are real aggregators! ## + self.agg_types = list(set(self.agg_types).intersection(self.func_set)) + copy_cats = copy.deepcopy(self.categoricals) + #### if categoricals is already a list, then start transforming ### + for i, each_catvar in enumerate(copy_cats): + try: + dft_cont = X[self.numerics+[each_catvar]] + except: + print(' %s columns given not found in data. Please correct your input.' %self.numerics) + return X + ### Then find the unique categories in the column ### + try: + #### This is where we create the aggregated features ######## + dft_full = dft_cont.groupby(each_catvar).agg(self.agg_types) + cols = [a +'_by_'+ str(each_catvar) +'_'+ b for (a,b) in dft_full.columns] + dft_full.columns = cols + except: + print(' Error: There are no unique categories in %s column. Skipping it...###' %each_catvar) + self.categoricals.remove(each_catvar) + continue + # make sure there are no zero-variance cols. If so, drop them # + #### drop zero variance cols the first time + copy_cols = copy.deepcopy(cols) + orig_shape = dft_full.shape[1] + for each_col in copy_cols: + if dft_full[each_col].var() == 0: + dft_full = dft_full.drop(each_col, axis=1) + num_cols_dropped = dft_full.shape[1] - orig_shape + num_cols_created = orig_shape - num_cols_dropped + print(' %d features grouped by %s for aggregates %s' %(num_cols_created, + each_catvar, self.agg_types)) + self.train_cols[each_catvar] = dft_full.columns.tolist() + self.transformers[each_catvar] = dft_full.reset_index() + + return self + + def transform(self, X, **fit_params): + + for i, each_catvar in enumerate(self.categoricals): + if len(self.train_cols[each_catvar]) == 0: + ## skip this variable if it has no transformed variables + continue + else: + ### now combine the aggregated variables with given dataset ### + dft_full = self.transformers[each_catvar] + ### simply fill in the missing values with the word "missing" ## + ### Remember that fillna only works at the dataframe level! + dft_full = dft_full.fillna(0) + try: + X = pd.merge(X, dft_full, on=each_catvar, how='left') + except: + for each_col in dft_full.columns.tolist(): + X[each_col] = 0.0 + print(' Erroring on creating aggregate vars for %s. Continuing...' %each_catvar) + continue + ### once all columns have been transferred return the dataframe ## + return X + + def fit_transform(self, X, **fit_params): + ### Since X for yT in a pipeline is sent as X, we need to switch X and y this way ## + self.fit(X) + for i, each_catvar in enumerate(self.categoricals): + if len(self.train_cols[each_catvar]) == 0: + ## skip this variable if it has no transformed variables + continue + else: + ### now combine the aggregated variables with given dataset ### + dft_full = self.transformers[each_catvar] + ### simply fill in the missing values with the word "missing" ## + ### Remember that fillna only works at the dataframe level! + dft_full = dft_full.fillna(0) + X = pd.merge(X, dft_full, on=each_catvar, how='left') + ### once all columns have been transferred return the dataframe ## + return X + + def inverse_transform(self, X, **fit_params): + ### One problem with this approach is that you have combined categories into one. + ### You cannot uncombine them since they no longer have a unique category. + ### You will get back the last transformed category when you inverse transform it. + print('There is no inverse transform for this aggregator...') + return X + + def predict(self, X, **fit_params): + #print('There is no predict function in Rare class combiner. Returning...') + return X +################################################################################### +import numpy as np +import pandas as pd +from sklearn.base import TransformerMixin, BaseEstimator +from collections import defaultdict +import pdb +import copy +from sklearn.base import TransformerMixin +from collections import defaultdict +class Ranking_Aggregator(BaseEstimator, TransformerMixin): + """ + ################################################################################################# + ###### This Ranking_Aggregator Class works just like any Transformer in sklearn ############### + ##### You can rank any ID column based on categorical columns in data. Why is it needed? ###### + ### If you have a patient in hospital then ranking them by city, state or illness is needed #### + ##### It uses the same fit() and fit_transform() methods of sklearn's LabelEncoder class. ##### + ### But you cannot use it in sklearn pipelines since they are more rigit in creating features ### + ################################################################################################# + ### This function is a very fast function that will iteratively compute rankings for ID vars ## + ### It returns original dataframe with added features using ID variables ranked by cat vars ### + ### What are aggregates? aggregates can be "count, "mean", "median", "mode", "min", "max", etc. + ### What do we aggregrate? all numeric columns in your data + ### What do we Rank? ID variables which are usually object or string varaiables. + ### Make sure to select uncorrelated features afterwards using FE_remove_variables_using_SULOV_method. + ################################################################################################# + ### Inputs: + ### dft: Just sent in the data frame df that you want features added to + ### agg_types: list of computational types: 'mean','median','count', 'max', 'min', 'sum', etc. + ### One caveat: these agg_types must be found in the following agg_func of numpy + ### or pandas groupby statements. + ### List of aggregates available: {'count','sum','mean','mad','median','min','max', + ### 'mode','abs', 'prod','std','var','sem','skew','kurt', + ### 'quantile','cumsum','cumprod','cummax','cummin'} + ### categoricals: columns to groupby all the numeric features and compute aggregates by. + ### idvars: columns that will ranked by categoricals above using aggregate types. + ### Outputs: + ### dataframe: The same input dataframe with additional features created by this function. + ################################################################################################# + Usage: + MGB = Ranking_Aggregator(categoricals=catcols,aggregates=['mean','skew'], idvars=idvars) + trainx = MGB.fit_transform(train) + testx = MGB.transform(test) + """ + def __init__(self, categoricals=[], aggregates=[], idvars=''): + # store the number of dimension of the target to predict an array of + # similar shape at predict + self.transformers = defaultdict(str) + self.categoricals = categoricals + self.agg_types = aggregates + self.idvars = idvars + self.train_cols = defaultdict(str) + self.func_set = {'average', 'min', 'max', 'dense', 'first'} + ### ‘first’ is not allowed for non-numeric variables ## + + def get_params(self, deep=True): + # This is to make it scikit-learn compatible #### + return {"categoricals": self.categoricals, "aggregates": self.agg_types, + "idvars": self.idvars} + + def set_params(self, **parameters): + for parameter, value in parameters.items(): + setattr(self, parameter, value) + return self + + def fit(self, X, **fit_params): + """Fit the model according to the given training data""" + try: + print('Beware: Potentially creates %d features (some will be dropped due to zero variance)' %( + len(self.categoricals)*len(self.agg_types))) + except Exception as e: + print('Erroring due to %s' %e) + ##### First make a copy of dataframe ### + dft_index = X.index + dft = copy.deepcopy(X) + # transformers are designed to modify X which must be multi-dimensional + if isinstance(X, pd.Series) or isinstance(X, np.ndarray): + print('Data cannot be a numpy array or a pandas Series. Must be dataframe!') + return X + if isinstance(self.categoricals, str): + self.categoricals = [self.categoricals] + if isinstance(self.idvars, str): + if self.idvars == 'all': + nunique_train = X.nunique().reset_index() + nunique_min = 0.20 + nunique_max = 0.4 + ID_limit_min = max(10, int(nunique_min*(len(X)))) ### X% of rows must be unique for it to be called ID + ID_limit_max = max(10, int(nunique_max*(len(X)))) ### X% of rows must be unique for it to be called ID + ls = nunique_train[(nunique_train[0]<=ID_limit_max) & (nunique_train[0]>=ID_limit_min) ]['index'].tolist() + if len(ls) > 0: + print(' Using first one from %s vars as ID vars since all option was chosen.' %ls) + self.idvars = ls[0] + else: + print(' No ID vars found that metet criteria of being %s-%s nuniques of dataset length. Returning' %(nunique_min, nunique_max)) + return self + else: + print(' %s ID variable chosen...' %self.idvars) + elif isinstance(self.idvars, list): + print(' only one ID variable can be chosen at a time. Choosing first one from list %s' %self.idvars) + self.idvars = self.idvars[0] + else: + print(' %s ID vars unrecognized. Please check your input and try again.' %self.idvars) + return self + ### Make sure the list of functions they send in are acceptable functions ## + ### Make sure that the aggregate functions are real aggregators! ## + self.agg_types = list(set(self.agg_types).intersection(self.func_set)) + dft_temp = dft[self.idvars] + ### Check if non-numeric dtype is used in dataset for ranking ## + if isinstance(dft_temp, pd.Series): + if not dft_temp.dtype.kind in 'biufc': + print(' "first" aggregate type not allowed in non-numeric columns') + if 'first' in self.agg_types: + self.agg_types.remove('first') + else: + for col in dft_temp.columns: + if not dft_temp[col].dtype.kind in 'biufc': + print(' "first" aggregate type not allowed in non-numeric columns') + if 'first' in self.agg_types: + self.agg_types.remove('first') + copy_cats = copy.deepcopy(self.categoricals) + #### if categoricals is already a list, then start transforming ### + for i, each_catvar in enumerate(copy_cats): + cols_added = [] + group_list = [self.idvars, each_catvar] + + ### Then find the unique categories in the column ### + + for each_type in self.agg_types: + new_col = str(self.idvars) + '_ranked_by_'+ str(each_catvar) + '_' + each_type + try: + df_temp = dft.groupby(group_list)[self.idvars].rank(method=each_type,ascending=True) + if df_temp.nunique() > 1: + dft[new_col] = df_temp.values + cols_added.append(new_col) + continue + except: + print('Error trying to add new aggregate column for %s by %s' %(each_catvar, each_type)) + + # make sure there are no zero-variance cols. If so, drop them # + + if len(cols_added) > 0: + copy_cols = copy.deepcopy(cols_added) + dft_full = pd.DataFrame() + dft_full = dft[[self.idvars,each_catvar]+cols_added].drop_duplicates(subset=[self.idvars,each_catvar],keep='first') + print(' %s columns added for %s' %(len(cols_added), each_catvar)) + self.train_cols[each_catvar] = cols_added + self.transformers[each_catvar] = dft_full + else: + print('No columns added for %s. Continuing...' %each_catvar) + continue + + del dft_full + del df_temp + + return self + + def transform(self, X, **fit_params): + for i, each_catvar in enumerate(self.categoricals): + + if len(self.train_cols[each_catvar]) == 0: + ## skip this variable if it has no transformed variables + continue + else: + if each_catvar in self.train_cols.keys(): + dft_full = pd.DataFrame() + ### now combine the aggregated variables with given dataset ### + cols_added = self.train_cols[each_catvar] + dft_full = self.transformers[each_catvar] + ### simply fill in the missing values with the word "0" ## + ### Remember that fillna only works at the dataframe level! + try: + X = pd.merge(X, dft_full, on=[self.idvars,each_catvar], how='left') + X[cols_added].fillna(0, inplace=True) + except: + for each_col in cols_added: + X[each_col] = 0.0 + print(' Erroring on creating aggregate vars for %s. Continuing...' %each_catvar) + continue + ### once all columns have been transferred return the dataframe ## + return X + + def fit_transform(self, X, **fit_params): + X = copy.deepcopy(X) + ### Since X for yT in a pipeline is sent as X, we need to switch X and y this way ## + self.fit(X) + for i, each_catvar in enumerate(self.categoricals): + if len(self.train_cols[each_catvar]) == 0: + ## skip this variable if it has no transformed variables + continue + else: + if each_catvar in self.train_cols.keys(): + dft_full = pd.DataFrame() + cols_added = self.train_cols[each_catvar] + ### now combine the aggregated variables with given dataset ### + dft_full = self.transformers[each_catvar] + ### simply fill in the missing values with the word "0" ## + ### Remember that fillna only works at the dataframe level! + if len(cols_added) > 0: + X = pd.merge(X, dft_full, on=[self.idvars,each_catvar], how='left') + X[cols_added].fillna(0, inplace=True) + ### once all columns have been transferred return the dataframe ## + return X + + def inverse_transform(self, X, **fit_params): + ### One problem with this approach is that you have combined categories into one. + ### You cannot uncombine them since they no longer have a unique category. + ### You will get back the last transformed category when you inverse transform it. + print('There is no inverse transform for this aggregator...') + return X + + def predict(self, X, **fit_params): + #print('There is no predict function in Rare class combiner. Returning...') + return X +################################################################################### +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +import copy +class DateTime_Transformer(BaseEstimator, TransformerMixin): + """ + ################################################################################################ + ###### The DateTime_Transformer class works just like sklearn's Transformers but better! ### + ##### It creates new features out of any date-time var in your dataset. It also handles NaN's## + ## The beauty of this function is that it takes care of NaN's and a variety of date formats.### + ##################### This is the BEST working version - don't mess with it!! ################## + ################################################################################################ + Usage: + ds = DateTime_Transformer(ts_column=col) + train = ds.fit_transform(train) ## this will give your transformed values as a dataframe + test = ds.transform(test) ### this will give your transformed values as a dataframe + + Usage in Column Transformers and Pipelines: + No. It cannot be used in pipelines since it needs to produce two columns for the next stage in pipeline. + See My_Label_Encoder_Pipe for an example of how to change this to use in sklearn pipelines. + """ + def __init__(self, ts_column, verbose=0): + self.ts_column = ts_column + self.verbose = verbose + self.cols_added = [] + self.fitted = False + + def fit(self, X, y=None): + X = copy.deepcopy(X) + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + print(' X must be dataframe. Converting it to a pd.DataFrame.') + X = pd.DataFrame(X.values, columns=[X.name]) + elif isinstance(X, np.ndarray): + X = pd.DataFrame(X) + print(' X must be dataframe. Converting it to a pd.DataFrame.') + else: + #### There is no way to transform dataframes since you will get a nested renamer error if you try ### + ### But if it is a one-dimensional dataframe, convert it into a Series + #print(' X is a DataFrame...') + pass + return self + + def transform(self, X, y=None): + X = copy.deepcopy(X) + if self.fitted: + ### This is for test data ########## + X_trans, _ = FE_create_time_series_features(X, self.ts_column, + ts_adds_in=self.cols_added, verbose=self.verbose) + return X_trans + else: + ### This is for train data ######### + self.fit(X) + X_trans, self.cols_added = FE_create_time_series_features(X, self.ts_column, + ts_adds_in=[], verbose=self.verbose) + self.fitted = True + return X_trans + + def fit_transform(self, X, y=None): + X = copy.deepcopy(X) + if self.fitted: + X_transformed = self.transform(X, y) + else: + self.fit(X, y) + X_transformed = self.transform(X, y) + return X_transformed +###################################################################################################### +import copy +def _create_ts_features(df, tscol, verbose=0): + """ + This takes in input a dataframe and a date variable. + It then creates time series features using the pandas .dt.weekday kind of syntax. + It also returns the data frame of added features with each variable as an integer variable. + """ + df = copy.deepcopy(df) + dt_adds = [] + try: + df[tscol+'_hour'] = df[tscol].dt.hour.fillna(0).astype(int) + df[tscol+'_minute'] = df[tscol].dt.minute.fillna(0).astype(int) + dt_adds.append(tscol+'_hour') + dt_adds.append(tscol+'_minute') + except: + print(' Error in creating hour-second derived features. Continuing...') + try: + df[tscol+'_dayofweek'] = df[tscol].dt.dayofweek.fillna(0).astype(int) + dt_adds.append(tscol+'_dayofweek') + if tscol+'_hour' in dt_adds: + DAYS = dict(zip(range(7),['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'])) + df[tscol+'_dayofweek'] = df[tscol+'_dayofweek'].map(DAYS) + df.loc[:,tscol+'_dayofweek_hour_cross'] = df[tscol+'_dayofweek'] +" "+ df[tscol+'_hour'].astype(str) + dt_adds.append(tscol+'_dayofweek_hour_cross') + df[tscol+'_quarter'] = df[tscol].dt.quarter.fillna(0).astype(int) + dt_adds.append(tscol+'_quarter') + df[tscol+'_month'] = df[tscol].dt.month.fillna(0).astype(int) + MONTHS = dict(zip(range(1,13),['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', + 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])) + df[tscol+'_month'] = df[tscol+'_month'].map(MONTHS) + dt_adds.append(tscol+'_month') + #### Add some features for months ######################################## + festives = ['Oct','Nov','Dec'] + name_col = tscol+"_is_festive" + df[name_col] = 0 + df[name_col] = df[tscol+'_month'].map(lambda x: 1 if x in festives else 0).values + ### Remember that fillna only works at dataframe level! ### + df[[name_col]] = df[[name_col]].fillna(0) + dt_adds.append(name_col) + summer = ['Jun','Jul','Aug'] + name_col = tscol+"_is_summer" + df[name_col] = 0 + df[name_col] = df[tscol+'_month'].map(lambda x: 1 if x in summer else 0).values + ### Remember that fillna only works at dataframe level! ### + df[[name_col]] = df[[name_col]].fillna(0) + dt_adds.append(name_col) + winter = ['Dec','Jan','Feb'] + name_col = tscol+"_is_winter" + df[name_col] = 0 + df[name_col] = df[tscol+'_month'].map(lambda x: 1 if x in winter else 0).values + ### Remember that fillna only works at dataframe level! ### + df[[name_col]] = df[[name_col]].fillna(0) + dt_adds.append(name_col) + cold = ['Oct','Nov','Dec','Jan','Feb','Mar'] + name_col = tscol+"_is_cold" + df[name_col] = 0 + df[name_col] = df[tscol+'_month'].map(lambda x: 1 if x in cold else 0).values + ### Remember that fillna only works at dataframe level! ### + df[[name_col]] = df[[name_col]].fillna(0) + dt_adds.append(name_col) + warm = ['Apr','May','Jun','Jul','Aug','Sep'] + name_col = tscol+"_is_warm" + df[name_col] = 0 + df[name_col] = df[tscol+'_month'].map(lambda x: 1 if x in warm else 0).values + ### Remember that fillna only works at dataframe level! ### + df[[name_col]] = df[[name_col]].fillna(0) + dt_adds.append(name_col) + ######################################################################### + if tscol+'_dayofweek' in dt_adds: + df.loc[:,tscol+'_month_dayofweek_cross'] = df[tscol+'_month'] +" "+ df[tscol+'_dayofweek'] + dt_adds.append(tscol+'_month_dayofweek_cross') + df[tscol+'_year'] = df[tscol].dt.year.fillna(0).astype(int) + dt_adds.append(tscol+'_year') + today = date.today() + df[tscol+'_age_in_years'] = today.year - df[tscol].dt.year.fillna(0).astype(int) + dt_adds.append(tscol+'_age_in_years') + df[tscol+'_dayofyear'] = df[tscol].dt.dayofyear.fillna(0).astype(int) + dt_adds.append(tscol+'_dayofyear') + df[tscol+'_dayofmonth'] = df[tscol].dt.day.fillna(0).astype(int) + dt_adds.append(tscol+'_dayofmonth') + df[tscol+'_weekofyear'] = df[tscol].dt.weekofyear.fillna(0).astype(int) + dt_adds.append(tscol+'_weekofyear') + weekends = (df[tscol+'_dayofweek'] == 'Sat') | (df[tscol+'_dayofweek'] == 'Sun') + df[tscol+'_typeofday'] = 'weekday' + df.loc[weekends, tscol+'_typeofday'] = 'weekend' + dt_adds.append(tscol+'_typeofday') + if tscol+'_typeofday' in dt_adds: + df.loc[:,tscol+'_month_typeofday_cross'] = df[tscol+'_month'] +" "+ df[tscol+'_typeofday'] + dt_adds.append(tscol+'_month_typeofday_cross') + except: + print(' Error in creating date time derived features. Continuing...') + if verbose: + print(' created %d columns from time series %s column' %(len(dt_adds),tscol)) + return df, dt_adds +################################################################ +from dateutil.relativedelta import relativedelta +from datetime import date +##### This is a little utility that computes age from year #### +def compute_age(year_string): + today = date.today() + age = relativedelta(today, year_string) + return age.years +################################################################# +def FE_create_time_series_features(dft, ts_column, ts_adds_in=[], verbose=0): + """ + FE stands for FEATURE ENGINEERING - That means this function will create new features! + ####### B E W A R E : H U G E N U M B E R O F F E A T U R E S ########### + This creates between 10 to 100 date time features for each date variable!! The number + of features created depends on whether it is just a year variable or a year+month+day variable + and has hours and minutes or seconds also. So this can create a huge number of features + using pandas date time column that you can send in. Optionally, you can send in a list + of columns that you want returned. It will use those same columns to ensure train and test + have the same number of columns and returns them in that order. + ###################################################################################### + Inputs: + dtf: pandas DataFrame + ts_column: name of the time series column + ts_adds_in: list of time series columns you want in the returned dataframe. + + Outputs: + dtf: The original pandas dataframe with new fields created by splitting date-time field + rem_ts_cols: List of added variables as output. This will be useful for future ts_adds_in + This list of columns is useful for matching test with train dataframes. + ###################################################################################### + """ + dtf = copy.deepcopy(dft) + reset_index = False + if not ts_adds_in: + # ts_column = None assumes that that index is the time series index + reset_index = False + if ts_column is None: + reset_index = True + ts_column = dtf.index.name + dtf = dtf.reset_index() + + ### In some extreme cases, date time vars are not processed yet and hence we must fill missing values here! + null_nums = dtf[ts_column].isnull().sum() + if null_nums > 0: + # missing_flag = True + new_missing_col = ts_column + '_Missing_Flag' + dtf[new_missing_col] = 0 + dtf.loc[dtf[ts_column].isnull(),new_missing_col]=1 + ### Remember that fillna only works at dataframe level! ### + dtf[[ts_column]] = dtf[[ts_column]].fillna(method='ffill') + print(' adding %s column due to missing values in data' %new_missing_col) + if dtf[dtf[ts_column].isnull()].shape[0] > 0: + ### Remember that fillna only works at dataframe level! ### + dtf[[ts_column]] = dtf[[ts_column]].fillna(method='bfill') + + if dtf[ts_column].dtype == float: + dtf[ts_column] = dtf[ts_column].astype(int) + + ### if we have already found that it was a date time var, then leave it as it is. Thats good enough! + items = dtf[ts_column].apply(str).apply(len).values + #### In some extreme cases, + if all(items[0] == item for item in items): + if items[0] == 4: + ### If it is just a year variable alone, you should leave it as just a year! + dtf[ts_column] = pd.to_datetime(dtf[ts_column],format='%Y') + ts_adds = [] + else: + ### if it is not a year alone, then convert it into a date time variable + dtf[ts_column] = pd.to_datetime(dtf[ts_column], infer_datetime_format=True) + ### this is where you create the time series features ##### + dtf, ts_adds = _create_ts_features(df=dtf, tscol=ts_column) + else: + dtf[ts_column] = pd.to_datetime(dtf[ts_column], infer_datetime_format=True) + ### this is where you create the time series features ##### + dtf, ts_adds = _create_ts_features(df=dtf, tscol=ts_column) + else: + dtf[ts_column] = pd.to_datetime(dtf[ts_column], infer_datetime_format=True) + ### this is where you create the time series features ##### + dtf, ts_adds = _create_ts_features(df=dtf, tscol=ts_column) + ####### This is where we make sure train and test have the same number of columns #### + try: + + if not ts_adds_in: + ts_adds_copy = copy.deepcopy(ts_adds) + rem_cols = left_subtract(dtf.columns.tolist(), ts_adds_copy) + ts_adds_num = dtf[ts_adds].select_dtypes(include='number').columns.tolist() + ### drop those columns where all rows are same i.e. zero variance #### + for col in ts_adds_num: + if dtf[col].std() == 0: + dtf = dtf.drop(col, axis=1) + ts_adds.remove(col) + removed_ts_cols = left_subtract(ts_adds_copy, ts_adds) + if verbose: + print(' dropped %d time series added columns due to zero variance' %len(removed_ts_cols)) + rem_ts_cols = ts_adds + dtf = dtf[rem_cols+rem_ts_cols] + else: + #rem_cols = left_subtract(dtf.columns.tolist(), ts_adds_in) + rem_cols = left_subtract(ts_adds, ts_adds_in) + dtf.drop(rem_cols, axis=1, inplace=True) + #dtf = dtf[rem_cols+ts_adds_in] + rem_ts_cols = ts_adds_in + # If you had reset the index earlier, set it back before returning + # to make it consistent with the dataframe that was sent as input + if reset_index: + dtf = dtf.set_index(ts_column) + elif ts_column in dtf.columns: + if verbose: + print(' dropping %s column after time series done' %ts_column) + dtf = dtf.drop(ts_column, axis=1) + else: + pass + if verbose: + print(' After dropping some zero variance cols, shape of data: %s' %(dtf.shape,)) + except Exception as e: + print('Error in Processing %s column due to %s for date time features. Continuing...' %(ts_column, e)) + return dtf, rem_ts_cols +###################################################################################### +from pandas.api.types import is_numeric_dtype +#gives fit_transform method for free +from sklearn.base import BaseEstimator, TransformerMixin +import copy +import pdb +class Binning_Transformer(BaseEstimator, TransformerMixin): + """ + ###### This is where we do ENTROPY BINNING OF CONTINUOUS VARS ########### + #### Best to do binning by using Target variables: that's why we use DT's + #### Make sure your input is pandas Series or DataFrame with all NUMERICS. + #### Otherwise Binning canot be done. This transformer ensures you get the + #### Best Results by generalizing using Regressors and Classifiers. + ############################################################################ + """ + def __init__(self, verbose=0): + self.verbose = verbose + ### This is where we set the max depth for setting defaults for clf ## + self.new_bincols = {} + self.entropy_threshold = {} + self.fitted = False + self.clfs = {} + self.max_number_of_classes = 1 + + def get_params(self, deep=True): + # This is to make it scikit-learn compatible #### + return {"verbose": self.verbose} + + def set_params(self, **parameters): + for parameter, value in parameters.items(): + setattr(self, parameter, value) + return self + + def num_classes(self, y): + """ + ### Returns number of classes in y + """ + from collections import defaultdict + from collections import OrderedDict + y = copy.deepcopy(y) + if isinstance(y, np.ndarray): + ls = pd.Series(y).nunique() + else: + if isinstance(y, pd.Series): + ls = y.nunique() + else: + if len(y.columns) >= 2: + ls = OrderedDict() + for each_i in y.columns: + ls[each_i] = y[each_i].nunique() + return ls + else: + ls = y.nunique()[0] + return ls + + + def fit(self, X, y, **fit_params): + from sklearn.tree import DecisionTreeRegressor, DecisionTreeClassifier + X = copy.deepcopy(X) + if isinstance(y, pd.DataFrame): + if len(y.columns) >= 2: + number_of_classes = self.num_classes(y) + for each_i in y.columns: + number_of_classes[each_i] = int(number_of_classes[each_i] - 1) + max_number_of_classes = np.max(list(number_of_classes.values())) + else: + number_of_classes = int(self.num_classes(y) - 1) + max_number_of_classes = np.max(number_of_classes) + else: + number_of_classes = int(self.num_classes(y) - 1) + max_number_of_classes = np.max(number_of_classes) + self.max_number_of_classes = max_number_of_classes + seed = 99 + if isinstance(X, np.ndarray): + print(' X cannot be numpy array. It must be either pandas Series or DataFrame!') + return self + elif isinstance(X, pd.Series): + self.continuous_vars = [X.name] + X = pd.DataFrame(X) + elif isinstance(X, pd.DataFrame): + self.continuous_vars = X.columns.tolist() + else: + print('Input seems to be of unknown data type. Returning...') + return self + ####### This is where we bin each variable through a method known as Entropy Binning ############## + X = X.fillna(-999) + for each_num in self.continuous_vars: + ### This is an Awesome Entropy Based Binning Method for Continuous Variables ########### + max_depth = max(2, int(np.log10(X[each_num].max()-X[each_num].min()))) + if is_numeric_dtype(y) and self.max_number_of_classes > 25: + clf = DecisionTreeRegressor(criterion='mse',min_samples_leaf=2, + max_depth=max_depth, + random_state=seed) + else: + clf = DecisionTreeClassifier(criterion='entropy',min_samples_leaf=2, + max_depth=max_depth, + random_state=seed) + try: + clf.fit(X[each_num].values.reshape(-1,1), y) + ranges = clf.tree_.threshold[clf.tree_.threshold>-2].tolist() + ranges.append(np.inf) + ranges.insert(0, -np.inf) + self.entropy_threshold[each_num] = np.sort(ranges) + self.new_bincols[each_num] = None + self.clfs[each_num] = clf + if self.verbose: + print(' %d bins created for %s...' %((len(ranges)-1), each_num)) + except: + self.entropy_threshold[each_num] = None + print('Skipping %s column for Entropy Binning due to Error. Check your input and try again' %each_num) + self.fitted = True + return self + + def transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + ####### This is where we bin each variable through a method known as Entropy Binning ############## + for each_num in self.continuous_vars: + if isinstance(X, pd.Series): + X = pd.DataFrame(X) + entropy_threshold = self.entropy_threshold[each_num] + if entropy_threshold is None: + print('skipping binning since there are no bins available for %s' %each_num) + continue + else: + try: + X[each_num] = np.digitize(X[each_num].values, entropy_threshold) + #### We Drop the original continuous variable after you have created the bin when Flag is true + self.new_bincols[each_num] = X[each_num].nunique() + except: + print('Error in %s during Entropy Binning' %each_num) + return X.values, y + + def fit_transform(self, X, y=None, **fit_params): + X = copy.deepcopy(X) + self.fit(X, y) + self.fitted = True + X_transformed, _ = self.transform(X, y) + return X_transformed, y +################################################################################################ +from pandas.api.types import is_numeric_dtype, is_integer_dtype +from pandas.api.types import is_datetime64_any_dtype +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +from sklearn.linear_model import LinearRegression +from xgboost import XGBRegressor +from tqdm import tqdm +from sklearn.metrics import r2_score +from collections import defaultdict +import pdb +import copy +############################################################################################## +class TS_Lagging_Transformer(BaseEstimator, TransformerMixin): + """ + ##################################################################################### + #### This is a PERFECT lagger based on matching current date to exact lag date #### + ##################################################################################### + ######### T I M E S E R I E S L A G G I N G T R A N S F O R M E R ############## + #### This is where we add Lags of Targets to Time Series data ############ + #### This only adds lags based on days. So if you want 365 days lag, you set ######## + #### lags = 365. The time series data can be in hourly, daily or weekly periods ##### + #### Lags help a model to learn how to predict future sales based on past data ###### + #### This is a very important feature engineering technique in time series data###### + ##################################################################################### + Inputs: + ---------------- + lags: number of lags based on days. So if you want 365 days lag, you set lags = 365. + ts_column: name of the date-time column. It should be a pandas date time column dtype. + It should be in your X dataframe. This will be used to set the time series index. + hier_vars: Names of hierarchical vars (id variables) such as user_id, store_id, item_id. + This is needed when you have 1000's of time series in your data. + Adding hier_vars will make your time series lags more accurate. + time_period: you can set it as "daily", "weekly", "hourly". This tells the lagger + that the time series is in daily, weekly or hourly format. + ############# This is where you use the Lagger to transform X and y ############## + X: is a dataframe with a pandas date-time variable and the hierarchical vars (optional) + y: You must send in a pandas series or dataframe. It must have the target column. + It will use y and join it with X to lag. This can be multi_label target also. + + Outputs: + ----------------- + X_transformed: This is the transformed data frame with lagged targets added. + """ + def __init__(self, lags, ts_column, hier_vars = [], time_period="", verbose=0): + self.lags = lags + self.ts_column = ts_column + if isinstance(ts_column, list): + print('Only one date column accepted. Taking the first col from list of cols given: %s' %ts_column[0]) + self.ts_column = ts_column[0] + else: + self.ts_column = ts_column + if isinstance(hier_vars, list): + if len(hier_vars) == 0: + print('No hierarchical vars given. Continuing without it but results may not be accurate...') + self.hier_vars = [] + else: + self.hier_vars = hier_vars + elif isinstance(hier_vars, str): + if hier_vars == '': + print('No hierarchical vars given. Continuing without it but results may not be accurate...') + self.hier_vars = [] + else: + self.hier_vars = [hier_vars] + else: + print('hier_vars must be a string or a list. Returning') + return + if time_period in ["daily", "weekly", "hourly"]: + self.time_period = time_period + else: + print("time period input must be either daily or weekly or hourly. Returning...") + return + self.verbose = verbose + self.targets = [] + self.fitted = False + self.train = None + self.X_index = None + self.ratio_col_adds = [] + self.y_prior = None + self.columns = [] + self.ratios = None + + def get_names_of_targets(self, y): + """ + ### Returns names of target variables in y if it is multi-label. + """ + y = copy.deepcopy(y) + if isinstance(y, np.ndarray): + y = pd.DataFrame(y, columns=['target'], index=self.X_index) + targets = ['target'] + if isinstance(y, pd.Series): + if y.name is None: + targets = ['target'] + y = pd.DataFrame(y, columns=targets, index=self.X_index) + else: + targets = [y.name] + y = pd.DataFrame(y, columns=targets, index=self.X_index) + elif isinstance(y, pd.DataFrame): + targets = y.columns.tolist() + self.targets = targets + return y + + + def fit(self, X, y): + X = copy.deepcopy(X) + y = copy.deepcopy(y) + ts_columns = [self.ts_column] + self.y_prior = copy.deepcopy(y) + self.X_index = X.index + self.columns = X.columns.tolist() + self.fitted = True + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + print('X must be dataframe and cannot be a pandas Series. Returning...') + return self + elif isinstance(X, np.ndarray): + print('X must be dataframe and cannot be a numpy array. Returning...') + return self + ########## you must save the product uniques so that train and test have consistent columns ## + print('Before adding lag column, shape of data set = %s' %(X.shape,)) + if not is_datetime64_any_dtype(X[self.ts_column]): + print('%s is not a pandas date-time dtype column. Converting it now.' %self.ts_column) + X[self.ts_column] = pd.to_datetime(X[self.ts_column]) + try: + self.train = X.join(self.get_names_of_targets(y)) + except: + print("Cannot join X to y. Check your inputs and try again...") + return self + if len(self.hier_vars) == 0: + self.ratios = self.train.groupby(ts_columns)[self.targets].sum() + else: + self.ratios = self.train.groupby(self.hier_vars+ts_columns)[self.targets].sum() + self.fitted = True + return self + + def transform(self, X, y=None): + self.test = copy.deepcopy(X) + X_trans = None + if self.fitted and y is None: + ############################################# + ##### This is for test data ################# + ############################################# + self.ratio_col_adds = [] + if is_datetime64_any_dtype(X[self.ts_column]): + try: + if self.time_period == 'daily': + self.test['new_'+self.ts_column] = self.test[self.ts_column] - pd.Timedelta(days=self.lags) + elif self.time_period == 'weekly': + self.test['new_'+self.ts_column] = self.test[self.ts_column] - pd.Timedelta(weeks=self.lags) + else: + self.test['new_'+self.ts_column] = self.test[self.ts_column] - pd.Timedelta(hours=self.lags) + #### Now we must transform using the new lag column ############ + for each_target in self.targets: + newcol = each_target+'_lag_'+str(self.lags) + if len(self.hier_vars) == 0: + self.test[newcol] = self.test.set_index(['new_'+self.ts_column]).index.map(self.ratios[each_target].get).fillna(0) + else: + self.test[newcol] = self.test.set_index(self.hier_vars+['new_'+self.ts_column]).index.map(self.ratios[each_target].get).fillna(0) + self.ratio_col_adds.append(newcol) + X_trans = self.test[self.columns + self.ratio_col_adds ] + print('After adding lag column, shape of data: %s' %(X_trans.shape,)) + return X_trans + except: + print(' Error occured in adding lag feature. Trying another method...') + else: + print('%s is not a pandas date-time dtype column. Converting it now.' %self.ts_column) + X[self.ts_column] = pd.to_datetime(X[self.ts_column]) + numtrans = Numeric_Transformer(ts_columns = [self.ts_column]) + #new_columns = left_subtract(self.X_transformed.columns.tolist(), self.ratio_col_adds) + new_columns = X.columns.tolist() + for each_ratio_col_add in self.ratio_col_adds: + X_train_1 = numtrans.fit_transform(X[new_columns], self.y_prior) + y_1 = self.X_transformed[each_ratio_col_add] + X_test_1 = numtrans.transform(X[new_columns]) + print('##### Training model to create %s column in test ############' %each_ratio_col_add) + xgbr = XGBRegressor(random_state=0) + # Train model using XGBRegressor + xgbr.fit(X_train_1, y_1) + # Make predictions + preds1 = xgbr.predict(X_test_1) + X[each_ratio_col_add] = preds1 + print(' Completed') + return X[self.columns + self.ratio_col_adds] + else: + ############################################## + ##### This is for train data ################# + ############################################## + try: + if self.time_period == 'daily': + self.train['new_'+self.ts_column] = self.train[self.ts_column] - pd.Timedelta(days=self.lags) + elif self.time_period == 'weekly': + self.train['new_'+self.ts_column] = self.train[self.ts_column] - pd.Timedelta(weeks=self.lags) + else: + self.train['new_'+self.ts_column] = self.train[self.ts_column] - pd.Timedelta(hours=self.lags) + #### Now we must transform using the new lag column ############ + for each_target in self.targets: + newcol = each_target+'_lag_'+str(self.lags) + if len(self.hier_vars) == 0: + self.train[newcol] = self.train.set_index(['new_'+self.ts_column]).index.map(self.ratios[each_target].get).fillna(0) + else: + self.train[newcol] = self.train.set_index(self.hier_vars+['new_'+self.ts_column]).index.map(self.ratios[each_target].get).fillna(0) + self.ratio_col_adds.append(newcol) + except: + print(' Error occured in adding lag feature. Check your inputs. Returning...') + return X + ##### This is where you set the end of training and return values ### + self.fitted = True + X_trans = self.train[self.columns + self.ratio_col_adds ] + print('After adding lag column, shape of data: %s' %(X_trans.shape,)) + return X_trans + + + def fit_transform(self, X, y, **fit_params): + X = copy.deepcopy(X) + y = copy.deepcopy(y) + if self.fitted: + return self.transform(X, y) + else: + self.fit(X, y) + return self.transform(X, y) + +################################################################################ +# THIS IS A PIPE version of the Transformers - it is useful for sklearn pipelines +class TS_Lagging_Transformer_Pipe(BaseEstimator, TransformerMixin): + """ + ##################################################################### + #### This is a PIPELINE version suitable for sklearn pipelines #### + ##################################################################### + """ + def __init__(self, lags, ts_column, hier_vars = [], time_period="", verbose=0): + self.lags = lags + self.ts_column = ts_column + self.hier_vars = hier_vars + self.time_period = time_period + self.verbose = verbose + + def fit(self, X, y): + tslag = TS_Lagging_Transformer(lags=self.lags, ts_column=self.ts_column, + hier_vars = self.hier_vars, time_period=self.time_period, verbose=self.verbose) + self.fitted = True + return tslag + + def transform(self, X, y=None): + if self.fitted: + return self.transform(X, y), y + else: + tslag = self.fit(X, y) + return tslag.transform(X), y + + def fit_transform(X, y, **fit_params): + tslag = self.fit(X, y) + return tslag.transform(X), y +################################################################################################# +class TS_Fourier_Transformer_Pipe(BaseEstimator, TransformerMixin): + """ + ######################################################################################### + #### This is a Fourier Transformer PIPELINE version suitable for sklearn pipelines #### + ######################################################################################### + """ + def __init__(self, ts_column, id_column='', time_period='daily', seasonality='1year', verbose=0): + self.ts_column = ts_column + self.id_column = id_column + self.time_period = time_period + self.seasonality = seasonality + self.verbose = verbose + + def fit(self, X, y): + tslag = TS_Fourier_Transformer(ts_column=self.ts_column, id_column=self.id_column, + time_period=self.time_period, seasonality=self.seasonality, verbose=self.verbose) + self.fitted = True + return tslag + + def transform(self, X, y=None): + if self.fitted: + return self.transform(X, y), y + else: + tslag = self.fit(X, y) + return tslag.transform(X), y + + def fit_transform(X, y, **fit_params): + tslag = self.fit(X, y) + return tslag.transform(X), y + +########################################################################################### +############## CONVERSION OF STRING COLUMNS TO NUMERIC using MY_LABELENCODER ######### +####################################################################################### +def FE_convert_all_object_columns_to_numeric(train, test="", features=[]): + """ + FE stands for Feature Engineering - it means this function performs feature engineering + ###################################################################################### + This is a utility that converts string columns to numeric using MY_LABEL ENCODER. + Make sure test and train have the same number of columns. If you have target in train, + remove it before sending it through this utility. Otherwise, might blow up during test transform. + The beauty of My_LabelEncoder is it handles NA's and future values in test that are not in train. + ####################################################################################### + Inputs: + train : pandas dataframe + test: (optional) pandas dataframe + + Outputs: + train: this is the transformed DataFrame + test: (optional) this is the transformed test dataframe if given. + ###################################################################################### + """ + + train = copy.deepcopy(train) + test = copy.deepcopy(test) + #### This is to fill all numeric columns with a missing number ########## + nums = train.select_dtypes('number').columns.tolist() + if len(features) == 0: + features = train.columns.tolist() + nums = [x for x in nums if x in features] + #### We don't want to look for ID columns and deleted columns ######## + if len(nums) == 0: + pass + else: + + if train[nums].isnull().sum().sum() > 0: + null_cols = np.array(nums)[train[nums].isnull().sum()>0].tolist() + for each_col in null_cols: + new_missing_col = each_col + '_Missing_Flag' + train[new_missing_col] = 0 + train.loc[train[each_col].isnull(),new_missing_col]=1 + ### Remember that fillna only works at dataframe level! ### + train[[each_col]] = train[[each_col]].fillna(-9999) + if not train[each_col].dtype in [np.float64,np.float32,np.float16]: + train[each_col] = train[each_col].astype(int) + if not isinstance(test, str): + if test is None: + pass + else: + new_missing_col = each_col + '_Missing_Flag' + test[new_missing_col] = 0 + test.loc[test[each_col].isnull(),new_missing_col]=1 + test[each_col] = test[each_col].fillna(-9999) + if not test[each_col].dtype in [np.float64,np.float32,np.float16]: + test[each_col] = test[each_col].astype(int) + ###### Now we convert all object columns to numeric ########## + lis = [] + error_columns = [] + + lis = train.select_dtypes('object').columns.tolist() + train.select_dtypes('category').columns.tolist() + if not isinstance(test, str): + if test is None: + pass + else: + lis_test = test.select_dtypes('object').columns.tolist() + test.select_dtypes('category').columns.tolist() + if len(left_subtract(lis, lis_test)) > 0: + ### if there is an extra column in train that is not in test, then remove it from consideration + lis = copy.deepcopy(lis_test) + if not (len(lis)==0): + for everycol in lis: + MLB = My_LabelEncoder() + try: + train_result = MLB.fit_transform(train[everycol]) + if isinstance(train_result, tuple): + train_result = train_result[0] + + train[everycol] = train_result + + if not isinstance(test, str): + if test is None: + pass + else: + test_result = MLB.transform(test[everycol]) + if isinstance(test_result, tuple): + test_result = test_result[0] + + test[everycol] = test_result + + except Exception as e: + print(f' error converting {everycol} column from string to numeric, deteail : {e}. Continuing...') + error_columns.append(everycol) + continue + + return train, test, error_columns +############################################################################################### +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +import copy +from sklearn.linear_model import LinearRegression +from xgboost import XGBRegressor +class TS_Trend_Seasonality_Transformer(BaseEstimator, TransformerMixin): + """ + This Transformer class will add a Trend and Seasonality column to your time series dataset. + In many retail use cases, you will need to add a column that aggregates sales + for each item as a percent of that store's sales that day or week. Similarly, + you may need to know sales of each item as a percent of the product category by date. + In these cases, you need a target column ("y") and a date column ("ts_column") + and finally you need a category column ("categorical_var") to aggregate. + We will always use "sum" as the aggregation function. The result will be a percent + column which you can add to your time series data set! + + Remember that this Transformer is very complex. Since we don't have the same dates + in test data as in train (since time series data are usually for forecasting problems). + We will do 2 things since test data has unseen dates and unseen stores compared to train data. + So you cannot do a left join to transfer data. You need to predict those columns in test using train data. + 1. First use X_transformed and set the y to be the ratios column. Then we use a linearn regression model + to train on train data and predict on test data. This will form the ratios column in test. + 2. Second use X_transformed and set y to the percent column. Then we use an XGBoost regression model + to train on train data and predict on test data. This will form the ratios column in test. + + Input: + ts_column: string. Must be an object or a pandas date-time dtype column. + Column must be found in the X input. Otherwise it will error. + categorical_var: string. default is "". group_id or product_id or store ID + that defines a group in the time series dataset. + verbose: default is 0. If set to 1, it will print more verbose output. + X: pandas dataframe. Must contain the time series column (as an object dtype) + and the categorical_var column which must be of object dtype. + y: pandas series or dataframe. Must contain the target (as an integer or float dtype). + """ + def __init__(self, ts_column, categorical_var="", verbose=0): + self.ts_column = ts_column + self.categorical_var = categorical_var + self.verbose = verbose + self.targets = [] + self.fitted = False + self.train = None + self.X_index = None + self.ratio_col_adds = [] + self.percent_col_adds = [] + self.columns = [] + self.ratios = None + + def get_names_of_targets(self, y): + """ + ### Returns names of target variables in y if it is multi-label. + """ + y = copy.deepcopy(y) + if isinstance(y, np.ndarray): + y = pd.DataFrame(y, columns=['target'], index=self.X_index) + targets = ['target'] + if isinstance(y, pd.Series): + if y.name is None: + targets = ['target'] + y = pd.DataFrame(y, columns=targets, index=self.X_index) + else: + targets = [y.name] + y = pd.DataFrame(y, columns=targets, index=self.X_index) + elif isinstance(y, pd.DataFrame): + targets = y.columns.tolist() + self.targets = targets + return y + + + def fit(self, X, y): + X = copy.deepcopy(X) + y = copy.deepcopy(y) + self.y_prior = copy.deepcopy(y) + self.X_index = X.index + self.columns = X.columns.tolist() + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + print('X must be dataframe and cannot be a pandas Series. Returning...') + return self + elif isinstance(X, np.ndarray): + print('X must be dataframe and cannot be a numpy array. Returning...') + return self + ########## you must save the product uniques so that train and test have consistent columns ## + if not is_datetime64_any_dtype(X[self.ts_column]): + print('%s is not a pandas date-time dtype column. Converting it now.' %self.ts_column) + X[self.ts_column] = pd.to_datetime(X[self.ts_column]) + print('Before adding trend and seasonality columns, shape of data set = %s' %(X.shape,)) + try: + self.train = X.join(self.get_names_of_targets(y)) + except: + print("Cannot join X to y. Check your inputs and try again...") + return self + self.ratios = (self.train.groupby([self.categorical_var,self.ts_column]).sum()/self.train.groupby( + [self.ts_column]).sum())[self.targets].to_dict() + self.fitted = True + return self + + def transform(self, X, y=None): + X = copy.deepcopy(X) + X_trans = None + ##### Then you should transform here ############ + if self.fitted and y is None: + if not is_datetime64_any_dtype(self.X_transformed[self.ts_column]): + print('%s is not a pandas date-time dtype column. Converting it now.' %self.ts_column) + self.X_transformed[self.ts_column] = pd.to_datetime(self.X_transformed[self.ts_column]) + numtrans = Numeric_Transformer(ts_columns = [self.ts_column]) + new_columns = left_subtract(self.X_transformed.columns.tolist(), self.ratio_col_adds + self.percent_col_adds) + X_train_1 = numtrans.fit_transform(self.X_transformed[new_columns], self.y_prior) + X_test_1 = numtrans.transform(X[new_columns]) + ### Since ts_column has been dropped, we need to subtract it from all columns ## + for each_ratio_col_add in self.ratio_col_adds: + y_1 = self.X_transformed[each_ratio_col_add] + print('##### Training model to create %s column in test ############' %each_ratio_col_add) + model1 = LinearRegression() + model1.fit(X_train_1, y_1) + preds1 = model1.predict(X_test_1) + X[each_ratio_col_add] = preds1 + print(' Completed') + for each_percent_col_add in self.percent_col_adds: + print('##### Training model to create %s column in test ############' %each_percent_col_add) + y_2 = self.X_transformed[each_percent_col_add] + X_train_2 = copy.deepcopy(X_train_1) + X_test_2 = copy.deepcopy(X_test_1) + xgbr = XGBRegressor(random_state=0) + # Train model using XGBRegressor + xgbr.fit(X_train_2, y_2) + # Make predictions + preds2 = xgbr.predict(X_test_2) + X[each_percent_col_add] = preds2 + print(' Completed') + return X[self.columns + self.ratio_col_adds + self.percent_col_adds] + try: + for each_target in self.targets: + self.train[each_target+'_'+self.categorical_var+'_trend'] = self.train.set_index( + [self.categorical_var, self.ts_column]).index.map(self.ratios[each_target].get) + self.train[each_target+'_'+self.categorical_var+'_seasonality'] = self.train[ + each_target]/self.train[each_target+'_'+self.categorical_var+'_trend'] + self.ratio_col_adds.append(each_target+'_'+self.categorical_var+'_trend') + self.percent_col_adds.append(each_target+'_'+self.categorical_var+'_seasonality') + except: + print(' Error occured in adding trend and seasonality features. Check your inputs. Returning...') + return X + ##### This is where you set the end of training and return values ### + self.fitted = True + X_trans = self.train[self.columns + self.ratio_col_adds + self.percent_col_adds] + print('After adding trend and seasonality columns, shape of data: %s' %(X_trans.shape,)) + return X_trans + + + def fit_transform(self, X, y): + X = copy.deepcopy(X) + y = copy.deepcopy(y) + if self.fitted: + self.X_transformed = self.transform(X, y) + else: + self.fit(X, y) + self.X_transformed = self.transform(X, y) + return self.X_transformed +############################################################################################################ +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +import copy +class TS_Fourier_Transformer(BaseEstimator, TransformerMixin): + """ + This Transformer class will add fourier transform features to daily and weekly time series data. + WARNING: You cannot use it for hourly or any other kind of time series data yet. + The time series must have a time column and an (optional) group identifier column. + It will return a dataset with Fourier transforms added for every day in a year and by group. + WARNING: If your test data does not contain any items (group_ids) from train data, + then all columns will be zero in test data since it cannot learn from train data. + In that case, you are better off selecting another group that is common to both + train and test data. The key to success is finding common groups within both! + + Input: + ts_column: string. Must be a date-time column. + Column must be in pandas date-time format. Otherwise will error. + id_column: string. default is "". group_id or product_id or store ID + that defines a group in the time series dataset. + time_period: string. default="daily". It will produce features based on dayofyear. + Use "weekly" to produce features based on weekofyear. + seasonality: string. default="1year". It will produce features for up to 1 year. + Use "2years" value will produce features for 2 years (max). + verbose: default is 0. If set to 1, it will print more verbose output. + """ + def __init__(self, ts_column, id_column="", time_period="daily", seasonality="1year", verbose=0): + self.ts_column = ts_column + self.id_column = id_column + self.time_period = time_period + self.seasonality = seasonality + if isinstance(self.seasonality, str): + if self.seasonality == "": + self.seasonality = "1year" + if isinstance(self.time_period, str): + if self.time_period == "": + self.time_period = "daily" + self.verbose = verbose + self.fitted = False + self.train = False + self.products = [] + self.listofyears = [] + self.dayofbiyear = None + + def fit(self, X, y=None): + X = copy.deepcopy(X) + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + if self.verbose: + print('X must be dataframe. Converting it to a pd.DataFrame.') + X = pd.DataFrame(X.values, columns=[X.name]) + elif isinstance(X, np.ndarray): + if self.verbose: + print('X must be dataframe and cannot be numpy array. Returning...') + return self + else: + #### There is no way to transform dataframes in an sklearn pipeline + #### since you will get a nested renamer error if you try ### + #### But if it is a one-dimensional dataframe, you can convert into Series + if self.verbose: + print('X is a DataFrame...') + pass + ########## you must save the product uniques so that train and test have consistent columns ## + self.products = X[self.id_column].unique().tolist() + print('Before Fourier features engg, shape of data = %s' %(X.shape,)) + self.fitted = True + return self + + def transform(self, X, y=None): + X = copy.deepcopy(X) + if self.fitted and self.train: + self.train = False + return self.X_transformed + ##### Then you should transform here ############ + # Time period could be 1year or 2year: otherwise it will assume 1 year. + if self.time_period == 'daily': + if self.seasonality == '1year': + self.dayofbiyear = X[self.ts_column].dt.dayofyear # 1 to 365 + self.listofyears = [2, 4] + elif self.seasonality == '2year': + self.dayofbiyear = X[self.ts_column].dt.dayofyear + 365*(1-(X[self.ts_column].dt.year%2)) # 1 to 730 + self.listofyears = [1, 2, 4] + elif self.time_period == 'weekly': + if self.seasonality == '1year': + self.dayofbiyear = X[self.ts_column].dt.weekofyear # 1 to 365 + self.listofyears = [2, 4] + elif self.seasonality == '2year': + self.dayofbiyear = X[self.ts_column].dt.weekofyear + 52*(1-(X[self.ts_column].dt.year%2)) # 1 to 104 + self.listofyears = [1, 2, 4] + ##### You need to reset the number of days above for each dataset ### + try: + print(' will create %s unique features...' %(len(self.products)*len(self.listofyears)*2)) + # k=1 -> 2 years, k=2 -> 1 year, k=4 -> 6 months + for k in self.listofyears: + if self.time_period == 'daily': + if self.seasonality == '1year': + X[f'sin{k}'] = np.sin(2 * np.pi * k * self.dayofbiyear / (1* 365)) + X[f'cos{k}'] = np.cos(2 * np.pi * k * self.dayofbiyear / (1* 365)) + else: + X[f'sin{k}'] = np.sin(2 * np.pi * k * self.dayofbiyear / (2* 365)) + X[f'cos{k}'] = np.cos(2 * np.pi * k * self.dayofbiyear / (2* 365)) + elif self.time_period == 'weekly': + if self.seasonality == '1year': + X[f'sin{k}'] = np.sin(2 * np.pi * k * self.dayofbiyear / (1* 52)) + X[f'cos{k}'] = np.cos(2 * np.pi * k * self.dayofbiyear / (1* 52)) + else: + X[f'sin{k}'] = np.sin(2 * np.pi * k * self.dayofbiyear / (2* 52)) + X[f'cos{k}'] = np.cos(2 * np.pi * k * self.dayofbiyear / (2* 52)) + + if self.id_column: ### only do this if they send in an ID column #### + #### we do this for Different items since each + #### has a different seasonality pattern + for product in self.products: + X[f'sin_{k}_{product}'] = X[f'sin{k}'] * (X[self.id_column] == product) + X[f'cos_{k}_{product}'] = X[f'cos{k}'] * (X[self.id_column] == product) + + X = X.drop([f'sin{k}', f'cos{k}'], axis=1) + print('After Fourier features engg, shape of data: %s' %(X.shape,)) + except: + print(' Error occured in adding Fourier features. Check your inputs. Returning...') + self.train = False + self.X_transformed = X + return self.X_transformed + ##### This is where you set the end of training and return values ### + self.fitted = True + self.train = True + self.X_transformed = X + return self.X_transformed + + + def fit_transform(self, X, y=None): + X = copy.deepcopy(X) + self.fit(X) + self.transform(X) + self.train = False + return self.X_transformed +######################################################################################################### +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +from pandas.api.types import is_object_dtype +import copy +import pdb +class Column_Names_Transformer(BaseEstimator, TransformerMixin): + """ + This Transformer class will make your column names unique. + Just fit on train data and transform on test data to make them same. + + Input: + train or X_train: a dataframe. Must have column names - should not be an array. + """ + def __init__(self, verbose=0): + self.verbose = verbose + self.old_column_names = [] + self.new_column_names = [] + self.rename_dict = {} + self.train = False + self.fitted = False + self.transformed_flag = False + + def fit(self, X): + X = copy.deepcopy(X) + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + if self.verbose: + print('X must be dataframe. Converting it to a pd.DataFrame.') + X = pd.DataFrame(X.values, columns=[X.name]) + elif isinstance(X, np.ndarray): + if self.verbose: + print('X must be dataframe and cannot be numpy array. Returning...') + return self + else: + #### There is no way to transform dataframes in an sklearn pipeline + #### since you will get a nested renamer error if you try ### + #### But if it is a one-dimensional dataframe, you can convert into Series + if self.verbose: + print('X is a DataFrame...') + pass + ########## you must save the product uniques so that train and test have consistent columns ## + if is_object_dtype(X.columns): + self.old_column_names = X.columns.tolist() + else: + X.columns = [str(x) for x in X.columns.tolist()] + self.old_column_names = X.columns.tolist() + if self.verbose: + print('Before making column names unique, shape of data = %s' %(X.shape,)) + self.new_column_names, self.transformed_flag = EDA_make_column_names_unique(X) + self.rename_dict = dict(zip(self.old_column_names, self.new_column_names)) + self.fitted = True + return self + + def transform(self, X, y=None): + + if self.fitted and self.train: + self.train = False + return self.X_transformed + ##### Then you should transform here ############ + if self.verbose: + print(' will make features unique...') + try: + self.X_transformed = copy.deepcopy(X) + self.X_transformed.rename(columns=self.rename_dict, inplace=True) + except: + print(' Error occured in making unique features. Check your inputs. Returning...') + self.train = False + self.X_transformed = X + return self.X_transformed + ##### This is where you set the end of training and return values ### + self.fitted = True + self.train = True + return self.X_transformed + + + def fit_transform(self, X, y=None): + X = copy.deepcopy(X) + self.fit(X) + self.transform(X) + self.train = False + return self.X_transformed +################################################################################ +import random +import collections +import re +import copy +def EDA_make_column_names_unique(data_input): + special_char_flag = False + cols = data_input.columns.tolist() + copy_cols = copy.deepcopy(cols) + ser = pd.Series(cols) + ### This function removes all special chars from a list ### + remove_special_chars = lambda x:re.sub('[^A-Za-z0-9_]+', '', x) + newls = ser.map(remove_special_chars).values.tolist() + ### there may be duplicates in this list - we need to make them unique by randomly adding strings to name ## + seen = [item for item, count in collections.Counter(newls).items() if count > 1] + new_cols = [x+str(random.randint(1,1000)) if x in seen else x for x in newls] + copy_new_cols = copy.deepcopy(new_cols) + copy_cols.sort() + copy_new_cols.sort() + if copy_cols != copy_new_cols: + print(' Some column names had special characters which were removed...') + special_char_flag = True + return new_cols, special_char_flag +########################################################################################## +from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin +import copy +class Numeric_Transformer(BaseEstimator, TransformerMixin): + """ + This Transformer class will convert all date-time, object and categorical columns to numeric. + It will leave numeric columns as is. + + Input: + ts_columns: list of names of date-time columns. These columns must be a pandas date-time dtype columns. + Columns must be found in the X input. Otherwise it will error. You can leave it as empty list. + verbose: default is 0. If set to 1, it will print more verbose output. + X: pandas dataframe. Must contain the time series columns (as date-time dtypes). Otherwise error! + All your object columns must be of object or categorical dtypes. + y: pandas series or dataframe. Must contain the target (as an integer or float dtype). + + Outputs: + X_transformed: It will return a transformed dataframe with all numeric columns. + + """ + def __init__(self, ts_columns=[], verbose=0): + self.ts_columns = ts_columns + self.verbose = verbose + self.lis = [] + self.columns = [] + self.error_columns = [] + self.mlbs = {} + self.dss = {} + self.fitted = False + + def fit(self, X, y=None): + X = copy.deepcopy(X) + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, tuple): + y = X[1] + X = X[0] + ### Now you can check if the parts of tuple are dataframe series, etc. + if isinstance(X, pd.Series): + print('X is a pandas Series. Converting...') + X = pd.DataFrame(X) + elif isinstance(X, np.ndarray): + print('X must be dataframe and cannot be a numpy array. Returning...') + return self + ########## you must save the product uniques so that train and test have consistent columns ## + print('Before converting columns, shape of data set = %s' %(X.shape,)) + self.lis = X.select_dtypes('object').columns.tolist() + X.select_dtypes('category').columns.tolist() + self.columns = X.columns.tolist() + return self + + def transform(self, X, y=None): + X = copy.deepcopy(X) + ### This is for both train and test data ######## + ### First we must convert all date-time columns to their parts of time #### + if len(self.ts_columns) > 0: + for every_ts in self.ts_columns: + ## we must convert date_col into date-time features before we convert object vars + if self.fitted: + ds1 = self.dss[every_ts] + X = ds1.transform(X) ### this will give your transformed values as a dataframe + else: + ds = DateTime_Transformer(ts_column=every_ts, verbose=1) + X = ds.fit_transform(X) ## this will give your transformed values as a dataframe + self.dss[every_ts] = ds + print('After converting all date columns to numeric, shape of data: %s' %(X.shape,)) + #### Now you have to convert all the columns into numeric ### + if not self.fitted: + self.columns = X.columns.tolist() + self.lis = X.select_dtypes('object').columns.tolist() + X.select_dtypes('category').columns.tolist() + if not (len(self.lis)==0): + for everycol in self.lis: + try: + if self.fitted: + MLB = self.mlbs[everycol] + train_result = MLB.transform(X[everycol]) + else: + MLB = My_LabelEncoder() + train_result = MLB.fit_transform(X[everycol]) + self.mlbs[everycol] = MLB + except Exception as e: + print(f' error converting {everycol} column from string to numeric, deteail : {e}. Continuing...') + self.error_columns.append(everycol) + continue + if isinstance(train_result, tuple): + train_result = train_result[0] + #### This is where you store the transformed column ##### + X[everycol] = train_result + else: + print('No categorical columns in X. Returning...') + return + ##### This is where you set the end of training and return values ### + self.fitted = True + self.columns = left_subtract(self.columns, self.error_columns) + print('After converting all date, object and category columns to numeric, shape of data: %s' %(X[self.columns].shape,)) + return X[self.columns] + + + def fit_transform(self, X, y): + X = copy.deepcopy(X) + y = copy.deepcopy(y) + if self.fitted: + X_transformed = self.transform(X, y) + else: + self.fit(X, y) + X_transformed = self.transform(X, y) + return X_transformed + +##################################################################################### +##################################################################################### diff --git a/build/lib/featurewiz/settings.py b/build/lib/featurewiz/settings.py new file mode 100644 index 0000000..5cb05f3 --- /dev/null +++ b/build/lib/featurewiz/settings.py @@ -0,0 +1,37 @@ +### this defines some of the global settings for encoder names in one place #### +from category_encoders import HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder +from category_encoders import OneHotEncoder, HelmertEncoder, OrdinalEncoder, CountEncoder, BaseNEncoder +from category_encoders import TargetEncoder, CatBoostEncoder, WOEEncoder, JamesSteinEncoder +from category_encoders.glmm import GLMMEncoder +from sklearn.preprocessing import LabelEncoder +from category_encoders.wrapper import PolynomialWrapper +from .encoders import FrequencyEncoder +################################################################################# +def init(): + global cat_encoders_names + cat_encoders_names = { + 'HashingEncoder': [HashingEncoder,'https://contrib.scikit-learn.org/category_encoders/hashing.html'], + 'SumEncoder': [SumEncoder,'https://contrib.scikit-learn.org/category_encoders/sum.html'], + 'PolynomialEncoder': [PolynomialEncoder,'https://contrib.scikit-learn.org/category_encoders/polynomial.html'], + 'BackwardDifferenceEncoder': [BackwardDifferenceEncoder,'https://contrib.scikit-learn.org/category_encoders/backward_difference.html'], + 'OneHotEncoder': [OneHotEncoder,'https://contrib.scikit-learn.org/category_encoders/onehot.html'], + 'HelmertEncoder': [HelmertEncoder,'https://contrib.scikit-learn.org/category_encoders/helmert.html'], + 'OrdinalEncoder': [OrdinalEncoder,'https://contrib.scikit-learn.org/category_encoders/ordinal.html'], + 'BaseNEncoder': [BaseNEncoder,'https://contrib.scikit-learn.org/category_encoders/basen.html'], + 'FrequencyEncoder': [FrequencyEncoder,'https://github.com/Alex-Lekov/AutoML_Alex/blob/master/automl_alex/encoders.py'], + } + + global target_encoders_names + target_encoders_names = { + 'TargetEncoder': [TargetEncoder,'https://contrib.scikit-learn.org/category_encoders/targetencoder.html'], + 'CatBoostEncoder': [CatBoostEncoder,'https://contrib.scikit-learn.org/category_encoders/catboost.html'], + 'WOEEncoder': [WOEEncoder,'https://contrib.scikit-learn.org/category_encoders/woe.html'], + 'JamesSteinEncoder': [JamesSteinEncoder,'https://contrib.scikit-learn.org/category_encoders/jamesstein.html'], + 'GLMMEncoder': [GLMMEncoder,'https://contrib.scikit-learn.org/category_encoders/glmm.html'], + } + + global modeltpe + modeltpe = '' + global multi_label + multi_label = False +################################################################################# diff --git a/build/lib/featurewiz/stacking_models.py b/build/lib/featurewiz/stacking_models.py new file mode 100644 index 0000000..2a0db4f --- /dev/null +++ b/build/lib/featurewiz/stacking_models.py @@ -0,0 +1,1397 @@ +import numpy as np +np.random.seed(42) +import random +random.seed(42) +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import warnings +warnings.filterwarnings('ignore') + +from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor +from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier +from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, ExtraTreeRegressor +from sklearn.linear_model import LogisticRegression, LinearRegression +from sklearn.kernel_ridge import KernelRidge +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import RobustScaler,StandardScaler,OneHotEncoder +from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, clone +from sklearn.base import ClassifierMixin +from sklearn.model_selection import KFold, cross_val_score, train_test_split +from sklearn.metrics import mean_squared_error,auc +from sklearn.svm import LinearSVC, LinearSVR +from sklearn.neural_network import MLPClassifier, MLPRegressor +from sklearn.ensemble import AdaBoostClassifier, AdaBoostRegressor +from sklearn.multioutput import MultiOutputRegressor, MultiOutputClassifier +from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor +from sklearn.discriminant_analysis import LinearDiscriminantAnalysis +from sklearn.linear_model import Lasso, LassoCV, Ridge, RidgeCV, LassoLarsCV +from sklearn.ensemble import ExtraTreesClassifier,ExtraTreesRegressor +#from sklearn.ensemble import HistGradientBoostingRegressor, HistGradientBoostingClassifier +from sklearn.multioutput import ClassifierChain, RegressorChain +from xgboost import XGBClassifier, XGBRegressor +from lightgbm import LGBMClassifier, LGBMRegressor +from sklearn.naive_bayes import MultinomialNB, GaussianNB + +from sklearn.model_selection import train_test_split +import pathlib +from scipy import stats +from scipy.stats import norm, skew +import time +import copy +import pdb +from collections import Counter +from collections import defaultdict +from collections import OrderedDict + +from sklearn.ensemble import GradientBoostingRegressor, RandomForestRegressor +from sklearn.pipeline import make_pipeline +from sklearn.preprocessing import RobustScaler + +######################################################################################### +def rmse(y_true,y_pred): + return np.sqrt(mean_squared_error(y_true,y_pred)) +################################################## +### Define the input models here ####### +################################################### +class Stacking_Classifier(BaseEstimator, ClassifierMixin, TransformerMixin): + """ + ################################################################################# + ############ Credit for Stacking Classifier ##################### + ################################################################################# + #### Greatly indebted to Gilbert Tanner who explained Stacked Models here + #### https://gilberttanner.com/blog/introduction-to-ensemble-learning + #### I used the blog to create a Stacking Classifier that can handle multi-label targets + ################################################################################# + """ + def __init__(self): + n_folds = 5 + use_features = False + self.base_models = [] + self.meta_model = None + self.n_folds = n_folds + self.use_features = use_features + self.target_len = 1 + + # We again fit the data on clones of the original models + def fit(self, X, y): + import lightgbm as lgb + models_dict = stacking_models_list(X_train=X, y_train=y, modeltype='Classification', verbose=1) + self.base_models = list(models_dict.values()) + self.base_models_ = [list() for x in self.base_models] + if y.ndim >= 2: + if y.shape[1] == 1: + self.meta_model = lgb.LGBMClassifier(n_estimators=100, random_state=99, n_jobs=-1) + else: + stump = lgb.LGBMClassifier(n_estimators=50, random_state=99) + self.meta_model = MultiOutputClassifier(stump, n_jobs=-1) + else: + self.meta_model = lgb.LGBMClassifier(n_estimators=100, random_state=99, n_jobs=-1) + self.meta_model_ = clone(self.meta_model) + + kfold = KFold(n_splits=self.n_folds, shuffle=True, random_state=156) + start_time = time.time() + model_name = str(self.meta_model).split("(")[0] + print('Stacking model %s training started. This will take time...' %model_name) + + # Train cloned base models then create out-of-fold predictions + # that are needed to train the cloned meta-model + # Train cloned base models and create out-of-fold predictions + if y.ndim <= 1: + self.target_len = 1 + else: + self.target_len = y.shape[1] + out_of_fold_predictions = np.zeros((X.shape[0], self.target_len*len(self.base_models))) + for i, model in enumerate(self.base_models): + start_time = time.time() + print(' %s model training and prediction...' %str(model).split("(")[0]) + + for train_index, holdout_index in kfold.split(X, y): + instance = clone(model) + self.base_models_[i].append(instance) + instance.fit(X.iloc[train_index], y.iloc[train_index]) + y_pred = instance.predict(X.iloc[holdout_index]) + if y.ndim == 1: + out_of_fold_predictions[holdout_index, i] = y_pred.ravel() + elif y.ndim <= 2: + if y.shape[1] == 1: + out_of_fold_predictions[holdout_index, i] = y_pred.ravel() + else: + next_i = int(i+self.target_len) + out_of_fold_predictions[holdout_index,i:next_i] = y_pred + else: + next_i = int(i+self.target_len) + out_of_fold_predictions[holdout_index,i:next_i] = y_pred + print(' Time taken = %0.0f seconds' %(time.time()-start_time)) + + if self.use_features: + self.meta_model_.fit(np.hstack((X, out_of_fold_predictions)), y) + else: + self.meta_model_.fit(out_of_fold_predictions, y) + + return self + + #Do the predictions of all base models on the test data and use the averaged predictions as + #meta-features for the final prediction which is done by the meta-model + def predict(self, X): + if self.target_len == 1: + meta_features = np.column_stack([ + np.column_stack([model.predict(X) for model in base_models]).mean(axis=1) + for base_models in self.base_models_ ]) + else: + max_len = self.target_len + base_models = self.base_models_[0] + for each_m, model in enumerate(base_models): + if each_m == 0: + stump_pred = model.predict(X) + pred = stump_pred[:] + else: + addl_pred = model.predict(X) + stump_pred = np.column_stack([stump_pred, addl_pred]) + for each_i in range(max_len): + next_i = int(each_i+self.target_len) + #pred[:,each_i] = np.column_stack([stump_pred[:,each_i],stump_pred[:,next_i]]).mean(axis=1) + pred[:,each_i] = (np.column_stack([stump_pred[:,each_i],stump_pred[:,next_i]]).mean(axis=1)>=0.5).astype(int) + meta_features = pred[:] + + if self.use_features: + return self.meta_model_.predict(np.hstack((X, meta_features))) + else: + return self.meta_model_.predict(meta_features) +################################################################### +class Stacking_Regressor(BaseEstimator, RegressorMixin, TransformerMixin): + """ + ################################################################################# + ############ Credit for Stacking Regressor ###################### + ################################################################################# + #### Greatly indebted to Gilbert Tanner who explained Stacked Models here + #### https://gilberttanner.com/blog/introduction-to-ensemble-learning + #### I used the blog to create a Stacking Regressor that can handle multi-label targets + ################################################################################# + """ + def __init__(self, use_features=True): + n_folds = 5 + self.base_models = [] + self.meta_model = None + self.n_folds = n_folds + self.use_features = use_features + self.target_len = 1 + + def fit(self, X, y): + """Fit all the models on the given dataset""" + + import lightgbm as lgb + models_dict = stacking_models_list(X_train=X, y_train=y, modeltype='Regression', verbose=1) + self.base_models = list(models_dict.values()) + self.base_models_ = [list() for x in self.base_models] + if y.ndim >= 2: + if y.shape[1] == 1: + self.meta_model = lgb.LGBMRegressor(n_estimators=50, random_state=99) + else: + stump = lgb.LGBMRegressor(n_estimators=50, random_state=99) + self.meta_model = MultiOutputRegressor(stump, n_jobs=-1) + else: + self.meta_model = lgb.LGBMRegressor(n_estimators=50, random_state=99) + self.meta_model_ = clone(self.meta_model) + + kfold = KFold(n_splits=self.n_folds, shuffle=True, random_state=42) + start_time = time.time() + model_name = str(self.meta_model).split("(")[0] + print('Stacking model %s training started. This will take time...' %model_name) + + # Train cloned base models and create out-of-fold predictions + if y.ndim <= 1: + self.target_len = 1 + else: + self.target_len = y.shape[1] + out_of_fold_predictions = np.zeros((X.shape[0], self.target_len*len(self.base_models))) + for i, model in enumerate(self.base_models): + print(' %s model training and prediction...' %str(model).split("(")[0]) + + start_time = time.time() + for train_index, holdout_index in kfold.split(X, y): + instance = clone(model) + self.base_models_[i].append(instance) + instance.fit(X.iloc[train_index], y.iloc[train_index]) + y_pred = instance.predict(X.iloc[holdout_index]) + + if y.ndim == 1: + out_of_fold_predictions[holdout_index, i] = y_pred.ravel() + elif y.ndim <= 2: + if y.shape[1] == 1: + out_of_fold_predictions[holdout_index, i] = y_pred.ravel() + else: + next_i = int(i+self.target_len) + out_of_fold_predictions[holdout_index,i:next_i] = y_pred + else: + next_i = int(i+self.target_len) + out_of_fold_predictions[holdout_index,i:next_i] = y_pred + print(' Time taken = %0.0f seconds' %(time.time()-start_time)) + + if self.use_features: + self.meta_model_.fit(np.hstack((X, out_of_fold_predictions)), y) + else: + self.meta_model_.fit(out_of_fold_predictions, y) + + return self + + def predict(self, X): + if self.target_len == 1: + meta_features = np.column_stack([ + np.column_stack([model.predict(X) for model in base_models]).mean(axis=1) + for base_models in self.base_models_ ]) + else: + max_len = self.target_len + base_models = self.base_models_[0] + for each_m, model in enumerate(base_models): + if each_m == 0: + stump_pred = model.predict(X) + pred = stump_pred[:] + else: + addl_pred = model.predict(X) + stump_pred = np.column_stack([stump_pred, addl_pred]) + for each_i in range(max_len): + next_i = int(each_i+self.target_len) + pred[:,each_i] = np.column_stack([stump_pred[:,each_i],stump_pred[:,next_i]]).mean(axis=1) + meta_features = pred[:] + + if self.use_features: + return self.meta_model_.predict(np.hstack((X, meta_features))) + else: + return self.meta_model_.predict(meta_features) +############################################################################### +class Blending_Regressor(BaseEstimator, RegressorMixin, TransformerMixin): + """ + ################################################################################# + ############ Credit for Blending Regressor ###################### + ################################################################################# + #### Greatly indebted to Gilbert Tanner who explained Stacked Models here + #### https://gilberttanner.com/blog/introduction-to-ensemble-learning + #### I used the blog to create a Blending Regressor that can handle multi-label targets + ################################################################################# + """ + def __init__(self, holdout_pct=0.2, use_features=True): + # create models + n_folds = 5 + self.base_models = [] + self.meta_model = None + self.n_folds = n_folds + self.holdout_pct = holdout_pct + self.use_features = use_features + self.target_len = 1 + + def fit(self, X, y): + import lightgbm as lgb + models_dict = stacking_models_list(X_train=X, y_train=y, modeltype='Regression', verbose=1) + self.base_models = list(models_dict.values()) + self.base_models_ = [clone(x) for x in self.base_models] + + if y.ndim >= 2: + if y.shape[1] == 1: + self.meta_model = lgb.LGBMRegressor(n_estimators=50, random_state=99, n_jobs=-1) + else: + stump = lgb.LGBMRegressor(n_estimators=50, random_state=99) + self.meta_model = MultiOutputRegressor(stump, n_jobs=-1) + else: + self.meta_model = lgb.LGBMRegressor(n_estimators=50, random_state=99, n_jobs=-1) + self.meta_model_ = clone(self.meta_model) + + start_time = time.time() + model_name = str(self.meta_model).split("(")[0] + print('Blending model %s training started. This will take time...' %model_name) + + X_train, X_holdout, y_train, y_holdout = train_test_split(X, y, test_size=self.holdout_pct) + if y.ndim <= 1: + self.target_len = 1 + else: + self.target_len = y.shape[1] + + #holdout_predictions = np.zeros((X_holdout.shape[0], self.target_len*len(self.base_models))) + + for i, model in enumerate(self.base_models_): + print(' %s model training and prediction...' %str(model).split("(")[0]) + start_time = time.time() + model.fit(X_train, y_train) + y_pred = model.predict(X_holdout) + print(' Time taken = %0.0f seconds' %(time.time()-start_time)) + if i == 0: + holdout_predictions = y_pred + else: + holdout_predictions = np.column_stack([holdout_predictions, y_pred]) + + if self.use_features: + if holdout_predictions.ndim < 2: + self.meta_model_.fit(np.hstack((X_holdout, holdout_predictions.reshape(-1,1))), y_holdout) + else: + self.meta_model_.fit(np.hstack((X_holdout, holdout_predictions)), y_holdout) + else: + self.meta_model_.fit(holdout_predictions, y_holdout) + + return self + + def predict(self, X): + #### This can handle multi_label predictions now ### + + if self.target_len == 1: + meta_features = np.column_stack([ + model.predict(X) for model in self.base_models_]) + else: + max_len = self.target_len + for each_m, model in enumerate(self.base_models_): + if each_m == 0: + stump_pred = model.predict(X) + pred = stump_pred[:] + else: + addl_pred = model.predict(X) + stump_pred = np.column_stack([stump_pred, addl_pred]) + for each_i in range(max_len): + next_i = int(each_i+self.target_len) + pred[:,each_i] = np.column_stack([stump_pred[:,each_i],stump_pred[:,next_i]]).mean(axis=1) + meta_features = pred[:] + + if self.use_features: + if meta_features.ndim < 2: + return self.meta_model_.predict(np.hstack((X, meta_features.reshape(-1,1)))) + else: + return self.meta_model_.predict(np.hstack((X, meta_features))) + else: + return self.meta_model_.predict(meta_features) +###################################################################################### +def find_rare_class(classes, verbose=0): + ######### Print the % count of each class in a Target variable ##### + """ + Works on Multi Class too. Prints class percentages count of target variable. + It returns the name of the Rare class (the one with the minimum class member count). + This can also be helpful in using it as pos_label in Binary and Multi Class problems. + """ + counts = OrderedDict(Counter(classes)) + total = sum(counts.values()) + if verbose >= 1: + print(' Class -> Counts -> Percent') + for cls in counts.keys(): + print("%6s: % 7d -> % 5.1f%%" % (cls, counts[cls], counts[cls]/total*100)) + if type(pd.Series(counts).idxmin())==str: + return pd.Series(counts).idxmin() + else: + return int(pd.Series(counts).idxmin()) +################################################################################ +def stacking_models_list(X_train, y_train, modeltype='Regression', verbose=0): + """ + Quickly build Stacks of multiple model results + Input must be a clean data set (only numeric variables, no categorical or string variables). + """ + import lightgbm as lgb + + X_train = copy.deepcopy(X_train) + y_train = copy.deepcopy(y_train) + start_time = time.time() + seed = 99 + if len(X_train) <= 100000 or X_train.shape[1] < 50: + NUMS = 100 + FOLDS = 5 + else: + NUMS = 200 + FOLDS = 10 + ## create Stacking models + estimators = [] + #### This is where you don't fit the model but just do cross_val_predict #### + if modeltype == 'Regression': + if y_train.ndim >= 2: + if y_train.shape[1] > 1: + stump = lgb.LGBMRegressor(n_estimators=50, random_state=99) + model1 = MultiOutputRegressor(stump, n_jobs=-1) + estimators.append(('Multi Output Regressor',model1)) + estimators_list = [(tuples[0],tuples[1]) for tuples in estimators] + estimator_names = [tuples[0] for tuples in estimators] + print('List of models chosen for stacking: %s' %estimators_list) + return dict(estimators_list) + ###### Bagging models if Bagging is chosen #### + model3 = LinearRegression(n_jobs=-1) + estimators.append(('Linear Model',model3)) + #### Tree models if Linear chosen ##### + model5 = DecisionTreeRegressor(random_state=seed,min_samples_leaf=2) + estimators.append(('Decision Trees',model5)) + #### Linear Models if Boosting is chosen ##### + model6 = ExtraTreeRegressor(random_state=seed) + estimators.append(('Extra Tree Regressor',model6)) + + #model7 = RandomForestRegressor(n_estimators=50,random_state=seed, n_jobs=-1) + model7 = Ridge(alpha=0.5) + estimators.append(('Ridge',model7)) + else: + ### This is for classification problems ######## + if y_train.ndim >= 2: + if y_train.shape[1] > 1: + stump = lgb.LGBMClassifier(n_estimators=50, random_state=99) + model1 = MultiOutputClassifier(stump, n_jobs=-1) + estimators.append(('Multi Output Classifier',model1)) + estimators_list = [(tuples[0],tuples[1]) for tuples in estimators] + estimator_names = [tuples[0] for tuples in estimators] + print('List of models chosen for stacking: %s' %estimators_list) + return dict(estimators_list) + ### Leave this as it is - don't change it ####### + n_classes = len(Counter(y_train)) + if n_classes > 2: + model3 = LogisticRegression(max_iter=5000, multi_class='ovr') + else: + model3 = LogisticRegression(max_iter=5000) + estimators.append(('Logistic Regression', model3)) + #### Linear Models if Boosting is chosen ##### + model4 = LinearDiscriminantAnalysis() + estimators.append(('Linear Discriminant',model4)) + + model5 = LGBMClassifier() + estimators.append(('LightGBM',model5)) + + ###### Naive Bayes models if Bagging is chosen #### + model7 = DecisionTreeClassifier(min_samples_leaf=2) + estimators.append(('Decision Tree',model7)) + + #### Create a new list here #################### + + estimators_list = [(tuples[0],tuples[1]) for tuples in estimators] + estimator_names = [tuples[0] for tuples in estimators] + print('List of models chosen for stacking: %s' %estimators_list) + return dict(estimators_list) +######################################################### +import copy +from collections import Counter +from sklearn.utils.class_weight import compute_class_weight +import pandas as pd +import numpy as np +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestClassifier, StackingClassifier +from sklearn.tree import DecisionTreeClassifier +from sklearn.linear_model import LogisticRegression +from sklearn.metrics import accuracy_score, f1_score, classification_report +from sklearn.utils import class_weight +from imblearn.over_sampling import ADASYN +from sklearn.base import BaseEstimator, TransformerMixin, RegressorMixin, ClassifierMixin +from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor +from xgboost import XGBClassifier, XGBRegressor +import pdb + +def analyze_problem_type_array(y_train, verbose=0) : + y_train = copy.deepcopy(y_train) + cat_limit = 30 ### this determines the number of categories to name integers as classification ## + float_limit = 15 ### this limits the number of float variable categories for it to become cat var + if y_train.ndim <= 1: + multi_label = False + else: + multi_label = True + #### This is where you detect what kind of problem it is ################# + if not multi_label: + if y_train.dtype in ['int64', 'int32','int16']: + if len(np.unique(y_train)) <= 2: + model_class = 'Binary_Classification' + elif len(y_train.unique()) > 2 and len(y_train.unique()) <= cat_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + elif y_train.dtype in ['float16','float32','float64']: + if len(y_train.unique()) <= 2: + model_class = 'Binary_Classification' + elif len(y_train.unique()) > 2 and len(y_train.unique()) <= float_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + else: + if len(y_train.unique()) <= 2: + model_class = 'Binary_Classification' + else: + model_class = 'Multi_Classification' + else: + for i in range(y_train.ndim): + ### if target is a list, then we should test dtypes a different way ### + if y_train.dtypes.all() in ['int64', 'int32','int16']: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + elif len(np.unique(y_train.iloc[:,0])) > 2 and len(np.unique(y_train.iloc[:,0])) <= cat_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + elif y_train.dtypes.all() in ['float16','float32','float64']: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + elif len(np.unique(y_train.iloc[:,0])) > 2 and len(np.unique(y_train.iloc[:,0])) <= float_limit: + model_class = 'Multi_Classification' + else: + model_class = 'Regression' + else: + if len(np.unique(y_train.iloc[:,0])) <= 2: + model_class = 'Binary_Classification' + else: + model_class = 'Multi_Classification' + ########### print this for the start of next step ########### + if multi_label: + print(''' %s %s problem ''' %('Multi_Label', model_class)) + else: + print(''' %s %s problem ''' %('Single_Label', model_class)) + return model_class, multi_label +############################################################################### +def train_evaluate_adasyn(X_train, y_train, X_test, y_test, final_estimator, + n_neighbors, sampling_strategy, class_weights_dict): + # ADASYN resampling + adasyn = ADASYN(n_neighbors=n_neighbors, sampling_strategy=sampling_strategy, random_state=42) + X_resampled, y_resampled = adasyn.fit_resample(X_train, y_train) + + # Simplified base estimators for the stacking classifier + base_estimators = [ + ('rf', RandomForestClassifier(class_weight=None, + n_estimators=100, + random_state=42)), + ('log_reg', LogisticRegression(random_state=42)), + ('dt', DecisionTreeClassifier(random_state=42)) + ] + + # Final estimator with class weights + if final_estimator is None: + #final_estimator = XGBClassifier(learning_rate=0.2, n_estimators=100, random_state=99) + final_estimator = RandomForestClassifier(class_weight=class_weights_dict, + n_estimators=100, + random_state=42) + + # Creating the Stacking Classifier with simplified base estimators + stacking_classifier = StackingClassifier(estimators=base_estimators, + final_estimator=final_estimator, cv=5) + + # Fitting the classifier on the resampled training data + stacking_classifier.fit(X_resampled, y_resampled) + + # Predicting on the test set + y_pred_resampled = stacking_classifier.predict(X_test) + + # Evaluating the classifier + accuracy_resampled = accuracy_score(y_test, y_pred_resampled) + f1_score_resampled = f1_score(y_test, y_pred_resampled, average='macro') + print(f"ADASYN Parameters - n_neighbors: {n_neighbors}, sampling_strategy: {sampling_strategy}") + print('F1 score macro = ', f1_score_resampled) + print(classification_report(y_test, y_pred_resampled)) + return stacking_classifier, f1_score_resampled + +class StackingClassifier_Multi(BaseEstimator, ClassifierMixin): + + def __init__(self, final_estimator=None): + print('initialized') + # Compute class weights + self.final_model = None + self.final_estimator = final_estimator + + def fit(self, X, y): + class_weights_dict = get_class_distribution(y_train) + modeltype, multi_label = analyze_problem_type_array(y_train) + + # Function to train and evaluate the model with ADASYN resampling + if modeltype == 'Regression': + X_train, X_val, y_train, y_val = train_test_split(X, y, + test_size=0.10, random_state=1,) + else: + X_train, X_val, y_train, y_val = train_test_split(X, y, + test_size=0.10, + stratify=y_train, + random_state=42) + print(' Train data: ', X_train.shape, ', Validation data: ', X_val.shape) + + # Parameters to try for ADASYN + n_neighbors_options = [5, 10] + ### do not use dictionary for multi-class since it doesn't work + ### do not try it because I have tried multiple things and they don't work + sampling_strategy = 'auto' + # Iterating over different combinations of ADASYN parameters + best_neighbors = 3 + best_sampling_strategy = 'auto' + f1_score_final = 0 + model_final = None + print('Model results on Validation data:') + try: + for n_neighbors in n_neighbors_options: + if np.unique(y_val).min() > n_neighbors: + n_neighbors = np.int(np.unique(y_val).min()-1) + model_temp, f1_score_temp = train_evaluate_adasyn(X_train, y_train, X_val, y_val, + final_estimator=self.final_estimator, + n_neighbors=n_neighbors, sampling_strategy=sampling_strategy, + class_weights_dict=class_weights_dict) + if f1_score_temp > f1_score_final: + best_neighbors = n_neighbors + f1_score_final = copy.deepcopy(f1_score_temp) + model_final = copy.deepcopy(model_temp) + except: + ### if it fails for any reason, just try auto and the smallest size of n_neighbors + model_temp, f1_score_temp = train_evaluate_adasyn(X_train, y_train, X_val, y_val, + final_estimator=self.final_estimator, + n_neighbors=3, sampling_strategy='auto', + class_weights_dict=class_weights_dict) + f1_score_final = copy.deepcopy(f1_score_temp) + model_final = copy.deepcopy(model_temp) + + print('best neighbors for ADASYN selected = ', best_neighbors) + ### training the final model on full X and y before sending it out + adasyn = ADASYN(n_neighbors=best_neighbors, + sampling_strategy=sampling_strategy, random_state=42) + X_resampled, y_resampled = adasyn.fit_resample(X, y) + model_final.fit(X_resampled, y_resampled) + self.final_model = model_final + return self + + def predict(self, X): + return self.final_model.predict(X) + + def predict_proba(self, X): + return self.final_model.predict_proba(X) +########################################################################## +from sklearn.base import BaseEstimator, TransformerMixin +import numpy as np + +class DenoisingAutoEncoder(BaseEstimator, TransformerMixin): + """ + A denoising autoencoder transformer for feature extraction from tabular datasets. + + This implementation is based on a research paper by: + Pascal Vincent et al. "Extracting and Composing Robust Features with Denoising Autoencoders" + Appearing in Proceedings of the 25th International Conference on Machine Learning, Helsinki, Finland, + 2008.Copyright 2008 by the author(s)/owner(s). + + This transformer adds noise to the input data and then trains an autoencoder to + reconstruct the original data, thereby learning robust features. It can automatically + select between a simple and a complex architecture based on the size of the dataset, + or this selection can be overridden by user input. + + Recommendation: Best is DAE with MinMax Scaling. But DAE_ADD also gives similar but slightly less performance. + + Parameters + ---------- + encoding_dim : int, default=50 + The size of the encoding layer. This determines the dimensionality of the output + features from the transformer. + + noise_factor : float, default=0.1 + The factor by which noise is added to the input data. Noise is generated from a + normal distribution and scaled by this factor. + + learning_rate : float, default=0.001 + The learning rate for the Adam optimizer used in training the autoencoder. + + epochs : int, default=100 + The number of epochs to train the autoencoder. + + batch_size : int, default=16 + The batch size used during the training of the autoencoder. + + callbacks : list of keras.callbacks.Callback, optional + Callbacks to apply during training of the autoencoder. + + simple_architecture : bool or None, default=None + If set to True, the transformer always uses a simple architecture regardless of the dataset size. + If set to False, it always uses a more complex architecture. If set to None, the architecture + is chosen automatically based on the dataset size (simple for less than 10,000 samples). + + Attributes + ---------- + autoencoder : keras.Model + The complete autoencoder model. + + encoder : keras.Model + The encoder part of the autoencoder model, used for feature extraction. + + Methods + ------- + fit(X, y=None) + Fit the transformer to the data. This method trains the autoencoder model. + + transform(X) + Apply the dimensionality reduction learned by the autoencoder, returning the encoded features. + + Examples + -------- + >>> from sklearn.preprocessing import MinMaxScaler + >>> scaler = MinMaxScaler() + >>> X_train_scaled = scaler.fit_transform(X_train) + >>> dae = DenoisingAutoEncoder() + >>> dae.fit(X_train_scaled, y_train) + >>> encoded_X_train = dae.transform(X_train_scaled) + >>> encoded_X_test = dae.transform(X_test_scaled) + + Notes: + Here are the recommende values for ae_options dictionary for DAE: + dae_dicto = { + 'noise_factor': 0.2, + 'encoding_dim': 10, + 'epochs': 100, + 'batch_size': 32, + 'simple_architecture': None + } + + """ + + def __init__(self, encoding_dim=50, noise_factor=0.1, + learning_rate=0.001, epochs=100, batch_size=16, + callbacks=None, simple_architecture=None): + try: + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow import keras + except: + print('tensorflow>= 2.5 not installed in machine. Please install and try again. ') + self.encoding_dim = encoding_dim + self.noise_factor = noise_factor + self.learning_rate = learning_rate + self.epochs = epochs + self.batch_size = batch_size + if callbacks is None: + es = keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=0.00001, patience=5, + verbose=1, mode='min', baseline=None, restore_best_weights=False) + self.callbacks = [es] + else: + self.callbacks = callbacks + self.simple_architecture = simple_architecture + self.autoencoder = None + self.encoder = None + + def _build_autoencoder(self, input_dim): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow.keras.layers import Input, Dense + from tensorflow.keras.models import Model + + input_layer = Input(shape=(input_dim,)) + if self.simple_architecture or (self.simple_architecture is None and input_dim < 10000): + print('Performing Denoising Auto Encoder transform using Simple architecture...') + encoded = Dense(self.encoding_dim, activation='relu')(input_layer) + decoded = Dense(input_dim, activation='sigmoid')(encoded) + else: + print('Performing Denoising Auto Encoder transform using Complex architecture...') + encoded = Dense(self.encoding_dim, activation='relu')(input_layer) + encoded = Dense(self.encoding_dim // 2, activation='relu')(encoded) + decoded = Dense(self.encoding_dim, activation='relu')(encoded) + decoded = Dense(input_dim, activation='sigmoid')(decoded) + + return Model(input_layer, decoded), Model(input_layer, encoded) + + def fit(self, X, y=None): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + # Add noise to the training data + X_noisy = X + self.noise_factor * np.random.normal(size=X.shape) + + # Build the autoencoder + self.autoencoder, self.encoder = self._build_autoencoder(X.shape[1]) + + # Compile and train the autoencoder + self.autoencoder.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=self.learning_rate), + loss='mean_squared_error') + self.autoencoder.fit(X_noisy, X, batch_size=self.batch_size, epochs=self.epochs, + callbacks=self.callbacks, shuffle=True, validation_split=0.20) + + return self + + def transform(self, X, y=None): + # Extract the features from the input data + encoded_X = self.encoder.predict(X) + return encoded_X +######################################################################### +import numpy as np +from sklearn.base import BaseEstimator, TransformerMixin + +class VariationalAutoEncoder(BaseEstimator, TransformerMixin): + """ + Variational Autoencoder (VAE) for feature extraction in multi-class classification problems. + + This transformer applies a VAE to the input data, which is beneficial for capturing + the underlying probability distribution of features. It's useful in scenarios like + imbalanced datasets, complex feature interactions, or when data augmentation is required. + + Recommendation: Try VAE with MinMax Scaling which is good. + Even better try VAE_ADD with MinMax scaling which might improve performance. + + Parameters + ---------- + intermediate_dim : int, default=64 + The dimension of the intermediate (hidden) layer in the encoder and decoder networks. + + latent_dim : int, default=4 + The dimension of the latent space (bottleneck layer). + + epochs : int, default=50 + The number of epochs for training the VAE. + + batch_size : int, default=128 + The batch size used during the training of the VAE. + + learning_rate : float, default=0.001 + The learning rate for the optimizer in the training process. + + Attributes + ---------- + vae : keras.Model + The complete Variational Autoencoder model. + + encoder : keras.Model + The encoder part of the VAE, used for feature extraction. + + Methods + ------- + fit(X, y=None) + Fit the transformer to the data. This method trains the VAE model. + + transform(X) + Apply the VAE to reduce the dimensionality of the data, returning the encoded features. + + Examples + -------- + >>> from sklearn.preprocessing import MinMaxScaler + >>> scaler = MinMaxScaler() + >>> X_train_scaled = scaler.fit_transform(X_train) + >>> vae = VariationalAutoEncoder() + >>> vae.fit(X_train_scaled) + >>> encoded_X_train = vae.transform(X_train_scaled) + + Notes: + Here are the recommended values for ae_options dictionary for VAE: + vae_dicto = { + 'intermediate_dim': 32, + 'latent_dim': 4, + 'epochs': 100, + 'batch_size': 32, + 'learning_rate': 0.001 + } + + """ + + def __init__(self, intermediate_dim=64, latent_dim=4, epochs=300, batch_size=64, learning_rate=0.001): + self.original_intermediate_dim = intermediate_dim + self.intermediate_dim = intermediate_dim + self.original_latent_dim = latent_dim + self.latent_dim = latent_dim + self.epochs = epochs + self.batch_size = batch_size + self.original_batch_size = batch_size + self.learning_rate = learning_rate + self.vae = None + self.encoder = None + try: + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow import keras + except: + print('tensorflow>= 2.5 not installed in machine. Please install and try again. ') + es = keras.callbacks.EarlyStopping(monitor='val_loss', min_delta=0.00001, patience=5, + verbose=1, mode='min', baseline=None, restore_best_weights=False) + # Learning rate scheduler + lr_scheduler = keras.callbacks.ReduceLROnPlateau(monitor='val_loss', factor=0.5, + patience=5, min_lr=0.0001) + + self.callbacks = [es, lr_scheduler] + + def _sampling(self, args): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow.keras import backend as K + z_mean, z_log_var = args + batch = K.shape(z_mean)[0] + dim = K.int_shape(z_mean)[1] + epsilon = K.random_normal(shape=(batch, dim)) + return z_mean + K.exp(0.5 * z_log_var) * epsilon + + def _build_vae(self, input_shape): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow.keras.layers import Input, Dense, Lambda + from tensorflow.keras.models import Model + from tensorflow.keras.losses import mse + from tensorflow.keras import backend as K + + # Manually specify the activation function of the last layer + # Adjust based on your model's specific configuration + last_layer_activation = 'sigmoid' # or 'linear', as appropriate + + # Encoder + inputs = Input(shape=input_shape, name='encoder_input') + x = Dense(self.intermediate_dim, activation='relu')(inputs) + z_mean = Dense(self.latent_dim, name='z_mean')(x) + z_log_var = Dense(self.latent_dim, name='z_log_var')(x) + + # Latent space + z = Lambda(self._sampling, output_shape=(self.latent_dim,), name='z')([z_mean, z_log_var]) + + # Instantiate the encoder + encoder = Model(inputs, [z_mean, z_log_var, z], name='encoder') + + # Decoder + latent_inputs = Input(shape=(self.latent_dim,), name='z_sampling') + x = Dense(self.intermediate_dim, activation='relu')(latent_inputs) + outputs = Dense(input_shape[0], activation=last_layer_activation)(x) + + # Instantiate the decoder + decoder = Model(latent_inputs, outputs, name='decoder') + + # VAE model + outputs = decoder(encoder(inputs)[2]) + vae = Model(inputs, outputs, name='vae') + + # Adjust the reconstruction loss depending on the activation function of the last layer + if last_layer_activation == 'sigmoid': + reconstruction_loss = mse(K.flatten(inputs), K.flatten(outputs)) + else: + reconstruction_loss = mse(inputs, outputs) + + reconstruction_loss *= input_shape[0] + kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var) + kl_loss = K.sum(kl_loss, axis=-1) + kl_loss *= -0.5 + epsilon = 1e-7 # Small epsilon for numerical stability + vae_loss = K.mean(reconstruction_loss + kl_loss + epsilon) + + vae.add_loss(vae_loss) + vae.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=self.learning_rate)) + return vae, encoder + + + def fit(self, X, y=None): + # Adjust dimensions if they exceed the number of features + n_features = X.shape[1] + self.intermediate_dim = min(self.original_intermediate_dim, int(3*n_features/4)) + self.latent_dim = min(self.original_latent_dim, int(n_features/2)) + + # Adjust batch size based on the sample size of X + n_samples = X.shape[0] + self.batch_size = min(self.original_batch_size, int(n_samples/10)) + + self.vae, self.encoder = self._build_vae((n_features,)) + print('Using Variational Auto Encoder to extract features...') + self.vae.fit(X, X, epochs=self.epochs, batch_size=self.batch_size, + callbacks=self.callbacks, shuffle=True, validation_split=0.20, + verbose=1) + return self + + def transform(self, X, y=None): + return self.encoder.predict(X)[0] +############################################################################### +import numpy as np +import pandas as pd + +class GAN: + """ + A Generative Adversarial Network (GAN) for generating synthetic data. + + This GAN implementation consists of a generator and a discriminator that are trained in tandem. The generator learns to produce data that resembles a given dataset, while the discriminator learns to distinguish between real and generated data. + + Parameters: + ---------- + input_dim : int (default=10) + The dimension of the input vector to the generator, typically the dimension of the noise vector. + + embedding_dim : int (default=50) + The dimension of the embeddings in the hidden layers of the generator and discriminator. + + output_dim : int (default same as X's number of features ) + The dimension of the output vector from the generator, which should match the dimension of the real data. + + epochs : int, optional (default=200) + The number of epochs to train the GAN. + + batch_size : int, optional (default=32) + The size of the batches used during training. + + Methods + ------- + fit(X, y=None) + Train the GAN on the given data. The method alternately trains the discriminator and the generator. + + generate_data(num_samples) + Generate synthetic data using the trained generator. + + Attributes + ---------- + generator : keras.Model + The generator component of the GAN. + + discriminator : keras.Model + The discriminator component of the GAN. + + Examples + -------- + >>> gan = GAN(input_dim=100, embedding_dim=50, output_dim=10, epochs=200, batch_size=32) + >>> gan.fit(X_train) + >>> synthetic_data = gan.generate_data(num_samples=1000) + + Notes + ----- + GANs are particularly useful for data augmentation, domain adaptation, and as + a component in more complex generative models. They require careful tuning of + parameters and architecture for stable and meaningful output. + + ### Early Stopping: The implementation of early stopping in GANs can be a bit tricky, + ### as it typically involves monitoring a validation metric. GANs don't use validation + ### data in the same way as traditional supervised learning models. + ### You might need to devise a custom criterion for early stopping based on + ### the generator's or discriminator's performance. + """ + try: + import tensorflow as tf + from tensorflow.keras.models import Model + except: + print('tensorflow>= 2.5 not installed in machine. Please install and try again. ') + + def __init__(self, input_dim, embedding_dim, output_dim, epochs=200, batch_size=32): + import tensorflow as tf + from tensorflow.keras.optimizers import Adam + from tensorflow.keras.models import Model + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + self.input_dim = input_dim + self.embedding_dim = embedding_dim + self.output_dim = output_dim + self.epochs = epochs + self.batch_size = batch_size + self.generator = self._build_generator() + self.discriminator = self._build_discriminator() + + # Initialize optimizers + self.gen_optimizer = Adam() + self.disc_optimizer = Adam() + + class Generator(Model): + def __init__(self, input_dim, embedding_dim, output_dim): + super().__init__() + from tensorflow.keras.layers import Dense, Input, Activation, Dropout + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + self.dense1 = Dense(embedding_dim, input_shape=(input_dim,)) + self.relu1 = Activation('relu') + self.dense2 = Dense(embedding_dim * 2) + self.relu2 = Activation('relu') + self.dense3 = Dense(output_dim) + self.sigmoid = Activation('sigmoid') + + def call(self, x): + from tensorflow.keras.layers import Dense, Input, Activation, Dropout + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + x = self.dense1(x) + x = self.relu1(x) + x = self.dense2(x) + x = self.relu2(x) + x = self.dense3(x) + return self.sigmoid(x) + + class Discriminator(Model): + def __init__(self, input_dim): + super().__init__() + from tensorflow.keras.layers import Dense, Input, Activation, Dropout + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + self.dense1 = Dense(input_dim, input_shape=(input_dim,)) + self.leaky_relu = Activation(tf.nn.leaky_relu) + self.dropout = Dropout(0.3) + self.dense2 = Dense(1) + self.sigmoid = Activation('sigmoid') + + def call(self, x): + x = self.dense1(x) + x = self.leaky_relu(x) + x = self.dropout(x) + x = self.dense2(x) + return self.sigmoid(x) + + def _build_generator(self): + return self.Generator(self.input_dim, self.embedding_dim, self.output_dim) + + def _build_discriminator(self): + return self.Discriminator(self.output_dim) + + def fit(self, X, y): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow.keras.models import Model + from tensorflow.keras.optimizers import Adam + from sklearn.base import BaseEstimator + from tensorflow.keras.losses import BinaryCrossentropy + + # Define the loss function + loss_fn = BinaryCrossentropy() + + for epoch in range(self.epochs): + # Shuffle the dataset + indices = np.arange(len(X)) + np.random.shuffle(indices) + X = X[indices] + + for i in range(0, len(X), self.batch_size): + real_data = X[i:i + self.batch_size] + noise = tf.random.normal([len(real_data), self.input_dim]) + + # Train Discriminator + with tf.GradientTape() as disc_tape: + fake_data = self.generator(noise, training=True) + real_output = self.discriminator(real_data, training=True) + fake_output = self.discriminator(fake_data, training=True) + + real_loss = loss_fn(tf.ones_like(real_output), real_output) + fake_loss = loss_fn(tf.zeros_like(fake_output), fake_output) + disc_loss = real_loss + fake_loss + + gradients_of_discriminator = disc_tape.gradient(disc_loss, self.discriminator.trainable_variables) + self.disc_optimizer.apply_gradients(zip(gradients_of_discriminator, self.discriminator.trainable_variables)) + + # Train Generator + with tf.GradientTape() as gen_tape: + generated_data = self.generator(noise, training=True) + gen_data_discriminated = self.discriminator(generated_data, training=True) + gen_loss = loss_fn(tf.ones_like(gen_data_discriminated), gen_data_discriminated) + + gradients_of_generator = gen_tape.gradient(gen_loss, self.generator.trainable_variables) + self.gen_optimizer.apply_gradients(zip(gradients_of_generator, self.generator.trainable_variables)) + + if epoch % 10 == 0: + ### print every 10 epochs ### + print(f"Epoch {epoch+1}, Gen Loss: {gen_loss.numpy()}, Disc Loss: {disc_loss.numpy()}") + + def generate_data(self, num_samples): + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + noise = tf.random.normal([num_samples, self.input_dim]) + return self.generator(noise).numpy() + +########################################################################### +from sklearn.base import BaseEstimator, TransformerMixin +from sklearn.utils import check_X_y + +class GANAugmenter(BaseEstimator, TransformerMixin): + """ + A scikit-learn-style transformer for augmenting tabular datasets using Generative Adversarial Networks (GANs). + + This transformer trains a separate GAN for each class in the dataset to generate synthetic data, which is then used to augment the dataset. It is particularly useful for handling imbalanced datasets by generating more samples for underrepresented classes. + + Parameters: + ---------- + gan_model : GAN class (default=None) + A GAN class that has fit and generate_data methods. You can leave it as None. + This class will be created and instantiated for each class in the dataset automatically. + + input_dim : int + Refers to the dimension of the input noise vector to the generator. This is a + hyperparameter that can be tuned. A larger input dimension can provide the + generator with more capacity to capture complex data distributions, but it + also increases the model's complexity and training time. + + embedding_dim : int + The dimension of the embeddings in the hidden layers of the GAN. + + epochs : int + The number of epochs to train each GAN. + + num_synthetic_samples : int + The number of synthetic samples to generate for each class. + + Methods + ------- + fit(X, y) + Fit the transformer to the data by training a separate GAN for each class found in y. + + transform(X, y) + Augment the data by generating synthetic data using the trained GANs and combining it with the original data. + + Attributes + ---------- + gans : dict + A dictionary storing the trained GANs for each class. + + Examples + -------- + >>> gan_model = GAN + >>> gan_augmenter = GANAugmenter(gan_model, embedding_dim=100, epochs=200, num_synthetic_samples=1000) + >>> gan_augmenter.fit(X_train, y_train) + >>> X_train_augmented, y_train_augmented = gan_augmenter.transform(X_train, y_train) + + Here are recommended values for ae_options for GANAugmenter: + gan_dicto = { + 'gan_model':None, + 'input_dim': 10, + 'embedding_dim': 100, + 'epochs': 100, + 'num_synthetic_samples': 400, + } + + Notes + ----- + The GANAugmenter is useful in scenarios where certain classes in a dataset are underrepresented. + By generating additional synthetic samples, it can help create a more balanced dataset, + which can be beneficial for training machine learning models. + + The GANAugmenter class initializes a pre-made GAN model where the input and output + dimensions are the same. This design choice is made under the assumption that the + GAN is being used for data augmentation, where the goal is typically to generate + synthetic data that has the same shape and structure as the original input data. + + In the context of data augmentation with GANs: Would Different Dimensions Improve Performance? + If you're referring to the input noise dimension, varying its size could potentially + impact the GAN's ability to learn complex data distributions. It's a hyperparameter + that can be experimented with. However, this doesn't directly translate to "better" + performance; it's more about finding the right capacity for the model to capture + the necessary level of detail in the data. + + The output dimension, however, should typically match the dimensionality of your + real data. Changing the output dimension would mean the generated data no longer + aligns with the feature space of your original dataset, which defeats the purpose + of augmentation for tasks like classification or regression on the same feature set. + + """ + + def __init__(self, gan_model=None, input_dim = None, + embedding_dim=100, epochs=200, num_synthetic_samples=1000): + """ + A transformer that trains GANs for each class to generate synthetic data. + + Parameters: + gan_model: A GAN model class with fit and generate_data methods. + embedding_dim: The embedding dimension for the GAN. + epochs: The number of training epochs for each GAN. + num_synthetic_samples: Number of synthetic samples to generate for each class. + """ + try: + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + from tensorflow.keras.optimizers import Adam + except: + print('tensorflow>= 2.5 not installed in machine. Please install and try again. ') + + if gan_model is None: + self.gan_model = GAN + else: + self.gan_model = gan_model + self.embedding_dim = embedding_dim + self.epochs = epochs + self.input_dim = input_dim + self.num_synthetic_samples = num_synthetic_samples + self.gans = {} + + def fit(self, X, y): + """ + Fit a separate GAN for each class in y. + + Parameters: + X: Feature matrix + y: Target vector + """ + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + import math + def rlog(x, base): + return math.log(x) / math.log(base) + + X, y = check_X_y(X, y) + + # Fit a GAN for each class + for class_label in np.unique(y): + X_class = X[y == class_label] + if self.input_dim is None: + ### if input dimension is not givem use the same size of X's features + self.input_dim = int(rlog(X_class.shape[1], 4))*5 + + ### Don't change this! this needs to be same as X's features + output_dim = X_class.shape[1] + + gan = self.gan_model(self.input_dim, self.embedding_dim, output_dim, self.epochs) + gan.fit(X_class, y) + + self.gans[class_label] = gan + + print('Input dimension given as %s for GAN. Try different input_dim values to tune GAN if needed.' %self.input_dim) + return self + + def transform(self, X, y): + """ + Generate synthetic data using the trained GANs and combine it with X. + + Parameters: + X: Feature matrix to be augmented + + Returns: + combined_data: The augmented feature matrix + combined_labels: The labels for the augmented feature matrix + """ + import tensorflow as tf + def set_seed(seed=42): + np.random.seed(seed) + random.seed(seed) + tf.random.set_seed(seed) + set_seed(42) + + synthetic_data_list = [] + synthetic_labels_list = [] + + for class_label, gan in self.gans.items(): + synthetic_data = gan.generate_data(self.num_synthetic_samples) + synthetic_data_list.append(synthetic_data) + synthetic_labels = np.full(self.num_synthetic_samples, class_label) + synthetic_labels_list.append(synthetic_labels) + + all_synthetic_data = np.vstack(synthetic_data_list) + all_synthetic_labels = np.concatenate(synthetic_labels_list) + + combined_data = np.vstack([X, all_synthetic_data]) + combined_labels = np.concatenate([y, all_synthetic_labels]) + + return combined_data, combined_labels +############################################################################# diff --git a/build/lib/featurewiz/sulov_method.py b/build/lib/featurewiz/sulov_method.py new file mode 100644 index 0000000..78cfeed --- /dev/null +++ b/build/lib/featurewiz/sulov_method.py @@ -0,0 +1,277 @@ +import numpy as np +import pandas as pd +import random +np.random.seed(99) +random.seed(42) +from . import settings +settings.init() +################################################################################ +#### The warnings from Sklearn are so annoying that I have to shut it off ####### +import warnings +warnings.filterwarnings("ignore") +from sklearn.exceptions import DataConversionWarning +warnings.filterwarnings(action='ignore', category=DataConversionWarning) +def warn(*args, **kwargs): + pass +warnings.warn = warn +import logging +#################################################################################### +import pdb +import copy +import time +from sklearn.feature_selection import chi2, mutual_info_regression, mutual_info_classif +from sklearn.feature_selection import SelectKBest +from itertools import combinations +import matplotlib.patches as mpatches +import matplotlib.pyplot as plt +################################################################################################# +from collections import defaultdict +from collections import OrderedDict +import time +################################################################################# +def left_subtract(l1,l2): + lst = [] + for i in l1: + if i not in l2: + lst.append(i) + return lst +################################################################################# +def return_dictionary_list(lst_of_tuples): + """ Returns a dictionary of lists if you send in a list of Tuples""" + orDict = defaultdict(list) + # iterating over list of tuples + for key, val in lst_of_tuples: + orDict[key].append(val) + return orDict +################################################################################ +def find_remove_duplicates(list_of_values): + """ + # Removes duplicates from a list to return unique values - USED ONLY ONCE + """ + output = [] + seen = set() + for value in list_of_values: + if value not in seen: + output.append(value) + seen.add(value) + return output +################################################################################## +def FE_remove_variables_using_SULOV_method(df, numvars, modeltype, target, + corr_limit = 0.70,verbose=0, dask_xgboost_flag=False): + """ + FE stands for Feature Engineering - it means this function performs feature engineering + ########################################################################################### + ##### SULOV stands for Searching Uncorrelated List Of Variables ############# + ########################################################################################### + SULOV method was created by Ram Seshadri in 2018. This highly efficient method removes + variables that are highly correlated using a series of pair-wise correlation knockout + rounds. It is extremely fast and hence can work on thousands of variables in less than + a minute, even on a laptop. You need to send in a list of numeric variables and that's + all! The method defines high Correlation as anything over 0.70 (absolute) but this can + be changed. If two variables have absolute correlation higher than this, they will be + marked, and using a process of elimination, one of them will get knocked out: + To decide order of variables to keep, we use mutuail information score to select. MIS returns + a ranked list of these correlated variables: when we select one, we knock out others that + are highly correlated to it. Then we select next variable to inspect. This continues until + we knock out all highly correlated variables in each set of variables. Finally we are + left with only uncorrelated variables that are also highly important in mutual score. + ########################################################################################### + ######## YOU MUST INCLUDE THE ABOVE MESSAGE IF YOU COPY SULOV method IN YOUR LIBRARY ########## + ########################################################################################### + """ + df = copy.deepcopy(df) + df_target = df[target] + df = df[numvars] + ### for some reason, doing a mass fillna of vars doesn't work! Hence doing it individually! + null_vars = np.array(numvars)[df.isnull().sum()>0] + for each_num in null_vars: + df[each_num] = df[each_num].fillna(0) + target = copy.deepcopy(target) + if verbose: + print('#######################################################################################') + print('##### Searching for Uncorrelated List Of Variables (SULOV) in %s features ############' %len(numvars)) + print('#######################################################################################') + print('Starting SULOV with %d features...' %len(numvars)) + ### This is a shorter version of getting unduplicated and highly correlated vars ## + #correlation_dataframe = df.corr().abs().unstack().sort_values().drop_duplicates() + ### This change was suggested by such3r on GitHub issues. Added Dec 30, 2022 ### + correlation_dataframe = df.corr().abs().unstack().sort_values().round(7).drop_duplicates() + corrdf = pd.DataFrame(correlation_dataframe[:].reset_index()) + corrdf.columns = ['var1','var2','coeff'] + corrdf1 = corrdf[corrdf['coeff']>=corr_limit] + ### Make sure that the same var is not correlated to itself! ### + corrdf1 = corrdf1[corrdf1['var1'] != corrdf1['var2']] + correlated_pair = list(zip(corrdf1['var1'].values.tolist(),corrdf1['var2'].values.tolist())) + corr_pair_dict = dict(return_dictionary_list(correlated_pair)) + corr_list = find_remove_duplicates(corrdf1['var1'].values.tolist()+corrdf1['var2'].values.tolist()) + keys_in_dict = list(corr_pair_dict.keys()) + reverse_correlated_pair = [(y,x) for (x,y) in correlated_pair] + reverse_corr_pair_dict = dict(return_dictionary_list(reverse_correlated_pair)) + #### corr_pair_dict is used later to make the network diagram to see which vars are correlated to which + for key, val in reverse_corr_pair_dict.items(): + if key in keys_in_dict: + if len(key) > 1: + corr_pair_dict[key] += val + else: + corr_pair_dict[key] = val + + ###### This is for ordering the variables in the highest to lowest importance to target ### + if len(corr_list) == 0: + final_list = list(correlation_dataframe) + print(' Selecting all (%d) variables since none of numeric vars are highly correlated...' %len(numvars)) + return numvars + else: + if isinstance(target, list): + target = target[0] + max_feats = len(corr_list) + if modeltype == 'Regression': + sel_function = mutual_info_regression + #fs = SelectKBest(score_func=sel_function, k=max_feats) + else: + sel_function = mutual_info_classif + #fs = SelectKBest(score_func=sel_function, k=max_feats) + ##### you must ensure there are no infinite nor null values in corr_list df ## + df_fit = df[corr_list] + ### Now check if there are any NaN values in the dataset ##### + + if df_fit.isnull().sum().sum() > 0: + df_fit = df_fit.dropna() + else: + print(' there are no null values in dataset...') + + if df_target.isnull().sum().sum() > 0: + print(' there are null values in target. Returning with all vars...') + return numvars + else: + print(' there are no null values in target column...') + + ##### Reduce memory usage and find mutual information score #### + #try: + # df_fit = reduce_mem_usage(df_fit) + #except: + # print('Reduce memory erroring. Continuing...') + ##### Ready to perform fit and find mutual information score #### + + try: + #fs.fit(df_fit, df_target) + if modeltype == 'Regression': + fs = mutual_info_regression(df_fit, df_target, n_neighbors=5, discrete_features=False, random_state=42) + else: + fs = mutual_info_classif(df_fit, df_target, n_neighbors=5, discrete_features=False, random_state=42) + except: + print(' SelectKBest() function is erroring. Returning with all %s variables...' %len(numvars)) + return numvars + try: + ################################################################################# + ####### This is the main section where we use mutual info score to select vars + ################################################################################# + #mutual_info = dict(zip(corr_list,fs.scores_)) + mutual_info = dict(zip(corr_list,fs)) + #### The first variable in list has the highest correlation to the target variable ### + sorted_by_mutual_info =[key for (key,val) in sorted(mutual_info.items(), key=lambda kv: kv[1],reverse=True)] + ##### Now we select the final list of correlated variables ########### + selected_corr_list = [] + #### You have to make multiple copies of this sorted list since it is iterated many times #### + orig_sorted = copy.deepcopy(sorted_by_mutual_info) + copy_sorted = copy.deepcopy(sorted_by_mutual_info) + copy_pair = copy.deepcopy(corr_pair_dict) + #### select each variable by the highest mutual info and see what vars are correlated to it + for each_corr_name in copy_sorted: + ### add the selected var to the selected_corr_list + selected_corr_list.append(each_corr_name) + for each_remove in copy_pair[each_corr_name]: + #### Now remove each variable that is highly correlated to the selected variable + if each_remove in copy_sorted: + copy_sorted.remove(each_remove) + ##### Now we combine the uncorrelated list to the selected correlated list above + rem_col_list = left_subtract(numvars,corr_list) + final_list = rem_col_list + selected_corr_list + removed_cols = left_subtract(numvars, final_list) + except Exception as e: + print(' SULOV Method crashing due to %s' %e) + #### Dropping highly correlated Features fast using simple linear correlation ### + removed_cols = remove_highly_correlated_vars_fast(df,corr_limit) + final_list = left_subtract(numvars, removed_cols) + if len(removed_cols) > 0: + if verbose: + print(' Removing (%d) highly correlated variables:' %(len(removed_cols))) + if len(removed_cols) <= 30: + print(' %s' %removed_cols) + if len(final_list) <= 30: + print(' Following (%d) vars selected: %s' %(len(final_list),final_list)) + ############## D R A W C O R R E L A T I O N N E T W O R K ################## + selected = copy.deepcopy(final_list) + if verbose: + try: + import networkx as nx + #### Now start building the graph ################### + gf = nx.Graph() + ### the mutual info score gives the size of the bubble ### + multiplier = 2100 + for each in orig_sorted: + gf.add_node(each, size=int(max(1,mutual_info[each]*multiplier))) + ######### This is where you calculate the size of each node to draw + sizes = [mutual_info[x]*multiplier for x in list(gf.nodes())] + #### The sizes of the bubbles for each node is determined by its mutual information score value + corr = df_fit.corr() + high_corr = corr[abs(corr)>corr_limit] + ## high_corr is the dataframe of a few variables that are highly correlated to each other + combos = combinations(corr_list,2) + ### this gives the strength of correlation between 2 nodes ## + multiplier = 20 + for (var1, var2) in combos: + if np.isnan(high_corr.loc[var1,var2]): + pass + else: + gf.add_edge(var1, var2,weight=multiplier*high_corr.loc[var1,var2]) + ######## Now start building the networkx graph ########################## + widths = nx.get_edge_attributes(gf, 'weight') + nodelist = gf.nodes() + cols = 5 + height_size = 5 + width_size = 15 + rows = int(len(corr_list)/cols) + if rows < 1: + rows = 1 + plt.figure(figsize=(width_size,min(20,height_size*rows))) + pos = nx.shell_layout(gf) + nx.draw_networkx_nodes(gf,pos, + nodelist=nodelist, + node_size=sizes, + node_color='blue', + alpha=0.5) + nx.draw_networkx_edges(gf,pos, + edgelist = widths.keys(), + width=list(widths.values()), + edge_color='lightblue', + alpha=0.6) + pos_higher = {} + x_off = 0.04 # offset on the x axis + y_off = 0.04 # offset on the y axis + for k, v in pos.items(): + pos_higher[k] = (v[0]+x_off, v[1]+y_off) + if len(selected) == 0: + nx.draw_networkx_labels(gf, pos=pos_higher, + labels=dict(zip(nodelist,nodelist)), + font_color='black') + else: + nx.draw_networkx_labels(gf, pos=pos_higher, + labels = dict(zip(nodelist,[x+' (selected)' if x in selected else x+' (removed)' for x in nodelist])), + font_color='black') + plt.box(True) + plt.title("""In SULOV, we repeatedly remove features with lower mutual info scores among highly correlated pairs (see figure), + SULOV selects the feature with higher mutual info score related to target when choosing between a pair. """, fontsize=10) + plt.suptitle('How SULOV Method Works by Removing Highly Correlated Features', fontsize=20,y=1.03) + red_patch = mpatches.Patch(color='blue', label='Bigger circle denotes higher mutual info score with target') + blue_patch = mpatches.Patch(color='lightblue', label='Thicker line denotes higher correlation between two variables') + plt.legend(handles=[red_patch, blue_patch],loc='best') + plt.show(); + ##### N E T W O R K D I A G R A M C O M P L E T E ################# + return final_list + except Exception as e: + print(' Networkx library visualization crashing due to %s' %e) + print('Completed SULOV. %d features selected' %len(final_list)) + else: + print('Completed SULOV. %d features selected' %len(final_list)) + return final_list +################################################################################### diff --git a/dist/featurewiz-0.5.0-py3-none-any.whl b/dist/featurewiz-0.5.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..ee22081e025e50498679e1d241d7340bba7134e0 GIT binary patch literal 129996 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 literal 0 HcmV?d00001 diff --git a/featurewiz.egg-info/PKG-INFO b/featurewiz.egg-info/PKG-INFO new file mode 100644 index 0000000..fd532bc --- /dev/null +++ b/featurewiz.egg-info/PKG-INFO @@ -0,0 +1,311 @@ +Metadata-Version: 2.1 +Name: featurewiz +Version: 0.5.0 +Summary: Select Best Features from your data set - any size - now with XGBoost! +Home-page: https://github.com/AutoViML/featurewiz +Author: Ram Seshadri +Author-email: rsesha2001@yahoo.com +License: Apache License 2.0 +Description: # featurewiz + `featurewiz` is the best feature selection library for boosting your machine learning performance with minimal effort and maximum relevance using the famous MRMR algorithm. + + ![banner](images/featurewiz_logos.png) + + # Table of Contents +

      + + ## Latest + If you are looking for the latest and greatest updates about our library, check out our updates page. +
      + + ## Citation + If you use featurewiz in your research project or paper, please use the following format for citations: +

      + "Seshadri, Ram (2020). GitHub - AutoViML/featurewiz: Use advanced feature engineering strategies and select the best features from your data set fast with a single line of code. source code: https://github.com/AutoViML/featurewiz " +

      + Current citations for featurewiz in [Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=) + + ## Introduction + `featurewiz` is a new python library for creating and selecting the best features in your data set fast! The differentiating features of featurewiz are: +

        +
      1. It provides one of the best automatic feature selection algorithms (Minimum Redundancy Maximum Relevance (MRMR) algorithm) as described by wikipedia in this page: "The MRMR selection has been found to be more powerful than the maximum relevance feature selection" such as Boruta.
      2. +
      3. It selects the best number of uncorrelated features that have maximum mutual information about the target without having to specify the number of features
      4. +
      5. It is fast and easy to use, and comes with a number of helpful features, such as a built-in categorical-to-numeric encoder and a powerful feature engineering module
      6. +
      7. It is well-documented, and it comes with a number of examples.
      8. +
      9. It is actively maintained, and it is regularly updated with new features and bug fixes.
      10. +
      + + `featurewiz` can be used in one or two ways. They are explained below. + ### 1. Feature Engineering +

      The first step is not absolutely necessary but it can be used to create new features that may or may not be helpful (be careful with automated feature engineering tools!).

      + One of the gaps in open-source AutoML tools and especially Auto_ViML has been the lack of feature engineering capabilities that high-powered competitions such as Kaggle required. The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables was difficult and sifting through those hundreds of new features to find the best features was difficult and left only to "experts" or "professionals". featurewiz was created to help you in this endeavor.
      +

      featurewiz now enables you to add hundreds of such features with a single line of code. Set the "feature_engg" flag to "interactions", "groupby" or "target" and featurewiz will select the best encoders for each of those options and create hundreds (perhaps thousands) of features in one go. Not only that, using the next step, featurewiz will sift through numerous such variables and find only the least correlated and most relevant features to your model. All in one step!.
      + + ![feature_engg](images/feature_engg.jpg) + + ### 2. Feature Selection +

      The second step is Feature Selection. `featurewiz` uses the MRMR (Minimum Redundancy Maximum Relevance) algorithm as the basis for its feature selection.
      + Why perform Feature Selection? Once you have created 100's of new features, you still have three questions left to answer: + 1. How do we interpret those newly created features? + 2. Which of these features is important and which is useless? How many of them are highly correlated to each other causing redundancy? + 3. Does the model overfit now on these new features and perform better or worse than before? +
      + All are very important questions and featurewiz answers them by using the SULOV method and Recursive XGBoost to reduce features in your dataset to the best "minimum optimal" features for the model.
      +

      SULOV: SULOV stands for `Searching for Uncorrelated List of Variables`. The SULOV algorithm is based on the Minimum-Redundancy-Maximum-Relevance (MRMR) algorithm explained in this article as one of the best feature selection methods. To understand how MRMR works and how it is different from `Boruta` and other feature selection methods, see the chart below. Here "Minimal Optimal" refers to MRMR (featurewiz) while "all-relevant" refers to Boruta.
      + + ![MRMR_chart](images/MRMR.png) + + ## Working + `featurewiz` performs feature selection in 2 steps. Each step is explained below. + The working of the `SULOV` algorithm is as follows: +

        +
      1. Find all the pairs of highly correlated variables exceeding a correlation threshold (say absolute(0.7)).
      2. +
      3. Then find their MIS score (Mutual Information Score) to the target variable. MIS is a non-parametric scoring method. So its suitable for all kinds of variables and target.
      4. +
      5. Now take each pair of correlated variables, then knock off the one with the lower MIS score.
      6. +
      7. What’s left is the ones with the highest Information scores and least correlation with each other.
      8. +
      + + ![sulov](images/SULOV.jpg) + + The working of the Recursive XGBoost is as follows: + Once SULOV has selected variables that have high mutual information scores with the least correlation among them, featurewiz uses XGBoost to repeatedly find the best features among the remaining variables after SULOV. +
        +
      1. Select all variables in the data set and the full data split into train and valid sets.
      2. +
      3. Find top X features (could be 10) on train using valid for early stopping (to prevent over-fitting)
      4. +
      5. Then take the next set of vars and find top X
      6. +
      7. Do this 5 times. Combine all selected features and de-duplicate them.
      8. +
      + + ![xgboost](images/xgboost.jpg) + + ## Tips + Here are some additional tips for ML engineers and data scientists when using featurewiz: +
        +
      1. How to cross-validate your results: When you use featurewiz, we automatically perform multiple rounds of feature selection using permutations on the number of columns. However, you can perform feature selection using permutations of rows as follows in cross_validate using featurewiz. +
      2. Use multiple feature selection tools: It is a good idea to use multiple feature selection tools and compare the results. This will help you to get a better understanding of which features are most important for your data.
      3. +
      4. Don't forget to engineer new features: Feature selection is only one part of the process of building a good machine learning model. You should also spend time engineering your features to make them as informative as possible. This can involve things like creating new features, transforming existing features, and removing irrelevant features.
      5. +
      6. Don't overfit your model: It is important to avoid overfitting your model to the training data. Overfitting occurs when your model learns the noise in the training data, rather than the underlying signal. To avoid overfitting, you can use regularization techniques, such as lasso or elasticnet.
      7. +
      8. Start with a small number of features: When you are first starting out, it is a good idea to start with a small number of features. This will help you to avoid overfitting your model. As you become more experienced, you can experiment with adding more features.
      9. +
      + + ## Install + + **Prerequisites:** +
        +
      1. featurewiz is built using xgboost, dask, numpy, pandas and matplotlib. It should run on most Python 3 Anaconda installations. You won't have to import any special libraries other than "dask", "XGBoost" and "networkx" library. Optionally, it uses LightGBM for fast modeling, which it installs automatically.
      2. +
      3. We use "networkx" library for charts and interpretability.
        But if you don't have these libraries, featurewiz will install those for you automatically.
      4. +
      + To install from source: + + ``` + cd + git clone git@github.com:AutoViML/featurewiz.git + # or download and unzip https://github.com/AutoViML/featurewiz/archive/master.zip + conda create -n python=3.7 anaconda + conda activate # ON WINDOWS: `source activate ` + cd featurewiz + pip install -r requirements.txt + ``` + + ## Good News: You can install featurewiz on Colab and Kaggle easily in 2 steps! + As of June 2022, thanks to [arturdaraujo](https://github.com/arturdaraujo), featurewiz is now available on conda-forge. You can try:
      + + ``` + conda install -c conda-forge featurewiz + ``` + + ### If the above conda install fails, you can try installing featurewiz this way: + #### Install featurewiz using git+
      + + ``` + !pip install git+https://github.com/AutoViML/featurewiz.git + ``` + + ## Usage + + There are two ways to use featurewiz. +
        +
      1. The first way is the new way where you use scikit-learn's `fit and predict` syntax. It also includes the `lazytransformer` library that I created to transform datetime, NLP and categorical variables into numeric variables automatically. We recommend that you use it as the main syntax for all your future needs.
      2. + + ``` + from featurewiz import FeatureWiz + fwiz = FeatureWiz(feature_engg = '', nrows=None, transform_target=True, scalers="std", + category_encoders="auto", add_missing=False, verbose=0) + X_train_selected, y_train = fwiz.fit_transform(X_train, y_train) + X_test_selected = fwiz.transform(X_test) + ### get list of selected features ### + fwiz.features + ``` + +
      3. The second way is the old way and this was the original syntax of featurewiz. It is still being used by thousands of researchers in the field. Hence it will continue to be maintained. However, it can be discontinued any time without notice. You can use it if you like it.
      4. + + ``` + import featurewiz as fwiz + outputs = fwiz.featurewiz(dataname=train, target=target, corr_limit=0.70, verbose=2, sep=',', + header=0, test_data='',feature_engg='', category_encoders='', + dask_xgboost_flag=False, nrows=None, skip_sulov=False, skip_xgboost=False) + ``` + + `outputs` is a tuple: There will always be two objects in output. It can vary: + - In the first case, it can be `features` and `trainm`: features is a list (of selected features) and trainm is the transformed dataframe (if you sent in train only) + - In the second case, it can be `trainm` and `testm`: It can be two transformed dataframes when you send in both test and train but with selected features. + + In both cases, the features and dataframes are ready for you to do further modeling. + + Featurewiz works on any multi-class, multi-label data Set. So you can have as many target labels as you want. + You don't have to tell Featurewiz whether it is a Regression or Classification problem. It will decide that automatically. + + ## API + + **Input Arguments for NEW syntax** + + Parameters + ---------- + corr_limit : float, default=0.90 + The correlation limit to consider for feature selection. Features with correlations + above this limit may be excluded. + + verbose : int, default=0 + Level of verbosity in output messages. + + feature_engg : str or list, default='' + Specifies the feature engineering methods to apply, such as 'interactions', 'groupby', + and 'target'. + + category_encoders : str or list, default='' + Encoders for handling categorical variables. Supported encoders include 'onehot', + 'ordinal', 'hashing', 'count', 'catboost', 'target', 'glm', 'sum', 'woe', 'bdc', + 'loo', 'base', 'james', 'helmert', 'label', 'auto', etc. + + add_missing : bool, default=False + If True, adds indicators for missing values in the dataset. + + dask_xgboost_flag : bool, default=False + If set to True, enables the use of Dask for parallel computing with XGBoost. + + nrows : int or None, default=None + Limits the number of rows to process. + + skip_sulov : bool, default=False + If True, skips the application of the Super Learning Optimized (SULO) method in + feature selection. + + skip_xgboost : bool, default=False + If True, bypasses the recursive XGBoost feature selection. + + transform_target : bool, default=False + When True, transforms the target variable(s) into numeric format if they are not + already. + + scalers : str or None, default=None + Specifies the scaler to use for feature scaling. Available options include + 'std', 'standard', 'minmax', 'max', 'robust', 'maxabs'. + + **Input Arguments for old syntax** + + - `dataname`: could be a datapath+filename or a dataframe. It will detect whether your input is a filename or a dataframe and load it automatically. + - `target`: name of the target variable in the data set. + - `corr_limit`: if you want to set your own threshold for removing variables as highly correlated, then give it here. The default is 0.9 which means variables less than -0.9 and greater than 0.9 in pearson's correlation will be candidates for removal. + - `verbose`: This has 3 possible states: + - `0` - limited output. Great for running this silently and getting fast results. + - `1` - verbose. Great for knowing how results were and making changes to flags in input. + - `2` - more charts such as SULOV and output. Great for finding out what happens under the hood for SULOV method. + - `test_data`: This is only applicable to the old syntax if you want to transform both train and test data at the same time in the same way. `test_data` could be the name of a datapath+filename or a dataframe. featurewiz will detect whether your input is a filename or a dataframe and load it automatically. Default is empty string. + - `dask_xgboost_flag`: default False. If you want to use dask with your data, then set this to True. + - `feature_engg`: You can let featurewiz select its best encoders for your data set by setting this flag + for adding feature engineering. There are three choices. You can choose one, two, or all three. + - `interactions`: This will add interaction features to your data such as x1*x2, x2*x3, x1**2, x2**2, etc. + - `groupby`: This will generate Group By features to your numeric vars by grouping all categorical vars. + - `target`: This will encode and transform all your categorical features using certain target encoders.
        + Default is empty string (which means no additional features) + - `add_missing`: default is False. This is a new flag: the `add_missing` flag will add a new column for missing values for all your variables in your dataset. This will help you catch missing values as an added signal. + - `category_encoders`: default is "auto". Instead, you can choose your own category encoders from the list below. + We recommend you do not use more than two of these. Featurewiz will automatically select only two if you have more than two in your list. You can set "auto" for our own choice or the empty string "" (which means no encoding of your categorical features)
        These descriptions are derived from the excellent category_encoders python library. Please check it out! + - `HashingEncoder`: HashingEncoder is a multivariate hashing implementation with configurable dimensionality/precision. The advantage of this encoder is that it does not maintain a dictionary of observed categories. Consequently, the encoder does not grow in size and accepts new values during data scoring by design. + - `SumEncoder`: SumEncoder is a Sum contrast coding for the encoding of categorical features. + - `PolynomialEncoder`: PolynomialEncoder is a Polynomial contrast coding for the encoding of categorical features. + - `BackwardDifferenceEncoder`: BackwardDifferenceEncoder is a Backward difference contrast coding for encoding categorical variables. + - `OneHotEncoder`: OneHotEncoder is the traditional Onehot (or dummy) coding for categorical features. It produces one feature per category, each being a binary. + - `HelmertEncoder`: HelmertEncoder uses the Helmert contrast coding for encoding categorical features. + - `OrdinalEncoder`: OrdinalEncoder uses Ordinal encoding to designate a single column of integers to represent the categories in your data. Integers however start in the same order in which the categories are found in your dataset. If you want to change the order, just sort the column and send it in for encoding. + - `FrequencyEncoder`: FrequencyEncoder is a count encoding technique for categorical features. For a given categorical feature, it replaces the names of the categories with the group counts of each category. + - `BaseNEncoder`: BaseNEncoder encodes the categories into arrays of their base-N representation. A base of 1 is equivalent to one-hot encoding (not really base-1, but useful), and a base of 2 is equivalent to binary encoding. N=number of actual categories is equivalent to vanilla ordinal encoding. + - `TargetEncoder`: TargetEncoder performs Target encoding for categorical features. It supports the following kinds of targets: binary and continuous. For multi-class targets, it uses a PolynomialWrapper. + - `CatBoostEncoder`: CatBoostEncoder performs CatBoost coding for categorical features. It supports the following kinds of targets: binary and continuous. For polynomial target support, it uses a PolynomialWrapper. This is very similar to leave-one-out encoding, but calculates the values “on-the-fly”. Consequently, the values naturally vary during the training phase and it is not necessary to add random noise. + - `WOEEncoder`: WOEEncoder uses the Weight of Evidence technique for categorical features. It supports only one kind of target: binary. For polynomial target support, it uses a PolynomialWrapper. It cannot be used for Regression. + - `JamesSteinEncoder`: JamesSteinEncoder uses the James-Stein estimator. It supports 2 kinds of targets: binary and continuous. For polynomial target support, it uses PolynomialWrapper. + For feature value i, James-Stein estimator returns a weighted average of: + The mean target value for the observed feature value i. + The mean target value (regardless of the feature value). + - `nrows`: default `None`. You can set the number of rows to read from your datafile if it is too large to fit into either dask or pandas. But you won't have to if you use dask. + - `skip_sulov`: default `False`. You can set the flag to skip the SULOV method if you want. + - `skip_xgboost`: default `False`. You can set the flag to skip the Recursive XGBoost method if you want. + + **Output values for old syntax** This applies only to the old syntax. + - `outputs`: Output is always a tuple. We can call our outputs in that tuple as `out1` and `out2` below. + - `out1` and `out2`: If you sent in just one dataframe or filename as input, you will get: + - 1. `features`: It will be a list (of selected features) and + - 2. `trainm`: It will be a dataframe (if you sent in a file or dataname as input) + - `out1` and `out2`: If you sent in two files or dataframes (train and test), you will get: + - 1. `trainm`: a modified train dataframe with engineered and selected features from dataname and + - 2. `testm`: a modified test dataframe with engineered and selected features from test_data. + + ## Additional + To learn more about how featurewiz works under the hood, watch this [video](https://www.youtube.com/embed/ZiNutwPcAU0) + + ![background](images/featurewiz_background.jpg) + + featurewiz was designed for selecting High Performance variables with the fewest steps. + In most cases, featurewiz builds models with 20%-99% fewer features than your original data set with nearly the same or slightly lower performance (this is based on my trials. Your experience may vary).
        +

        + featurewiz is every Data Scientist's feature wizard that will:

          +
        1. Automatically pre-process data: you can send in your entire dataframe "as is" and featurewiz will classify and change/label encode categorical variables changes to help XGBoost processing. It classifies variables as numeric or categorical or NLP or date-time variables automatically so it can use them correctly to model.
          +
        2. Perform feature engineering automatically: The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables is difficult and sifting through those hundreds of new features is painstaking and left only to "experts". Now, with featurewiz you can create hundreds or even thousands of new features with the click of a mouse. This is very helpful when you have a small number of features to start with. However, be careful with this option. You can very easily create a monster with this option. +
        3. Perform feature reduction automatically. When you have small data sets and you know your domain well, it is easy to perhaps do EDA and identify which variables are important. But when you have a very large data set with hundreds if not thousands of variables, selecting the best features from your model can mean the difference between a bloated and highly complex model or a simple model with the fewest and most information-rich features. featurewiz uses XGBoost repeatedly to perform feature selection. You must try it on your large data sets and compare!
          +
        4. Explain SULOV method graphically using networkx library so you can see which variables are highly correlated to which ones and which of those have high or low mutual information scores automatically. Just set verbose = 2 to see the graph.
          +
        5. Build a fast XGBoost or LightGBM model using the features selected by featurewiz. There is a function called "simple_lightgbm_model" which you can use to build a fast model. It is a new module, so check it out.
          +
        + + *** Special thanks to fellow open source Contributors ***:
        +
          +
        1. Alex Lekov (https://github.com/Alex-Lekov/AutoML_Alex/tree/master/automl_alex) for his DataBunch and encoders modules which are used by the tool (although with some modifications).
        2. +
        3. Category Encoders library in Python : This is an amazing library. Make sure you read all about the encoders that featurewiz uses here: https://contrib.scikit-learn.org/category_encoders/index.html
        4. +
        + + ## Maintainers + + * [@AutoViML](https://github.com/AutoViML) + + ## Contributing + + See [the contributing file](CONTRIBUTING.md)! + + PRs accepted. + + ## License + + Apache License 2.0 © 2020 Ram Seshadri + + ## DISCLAIMER + This project is not an official Google project. It is not supported by Google and Google specifically disclaims all warranties as to its quality, merchantability, or fitness for a particular purpose. + + + [page]: examples/cross_validate.py + +Platform: UNKNOWN +Classifier: Programming Language :: Python :: 3 +Classifier: Operating System :: OS Independent +Description-Content-Type: text/markdown diff --git a/featurewiz.egg-info/SOURCES.txt b/featurewiz.egg-info/SOURCES.txt new file mode 100644 index 0000000..d3d0683 --- /dev/null +++ b/featurewiz.egg-info/SOURCES.txt @@ -0,0 +1,18 @@ +README.md +setup.py +featurewiz/__init__.py +featurewiz/__version__.py +featurewiz/classify_method.py +featurewiz/databunch.py +featurewiz/encoders.py +featurewiz/featurewiz.py +featurewiz/ml_models.py +featurewiz/my_encoders.py +featurewiz/settings.py +featurewiz/stacking_models.py +featurewiz/sulov_method.py +featurewiz.egg-info/PKG-INFO +featurewiz.egg-info/SOURCES.txt +featurewiz.egg-info/dependency_links.txt +featurewiz.egg-info/requires.txt +featurewiz.egg-info/top_level.txt \ No newline at end of file diff --git a/featurewiz.egg-info/dependency_links.txt b/featurewiz.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/featurewiz.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/featurewiz.egg-info/requires.txt b/featurewiz.egg-info/requires.txt new file mode 100644 index 0000000..7885e68 --- /dev/null +++ b/featurewiz.egg-info/requires.txt @@ -0,0 +1,20 @@ +Pillow>=9.0.0 +category_encoders>=2.6.2 +dask>=2021.11.0 +distributed>=2021.11.0 +feather-format>=0.4.1 +fsspec>=0.3.3 +ipython +jupyter +lazytransform>=1.10 +lightgbm>=3.2.1 +matplotlib +networkx>=2.6.2 +numexpr>=2.7.3 +pandas<2.0,>=1.3.4 +pyarrow>=7.0.0 +scikit-learn<=1.2.2,>=0.24 +seaborn +tqdm>=4.61.1 +xgboost<=1.6.2,>=1.5 +xlrd>=2.0.0 diff --git a/featurewiz.egg-info/top_level.txt b/featurewiz.egg-info/top_level.txt new file mode 100644 index 0000000..c4adda7 --- /dev/null +++ b/featurewiz.egg-info/top_level.txt @@ -0,0 +1 @@ +featurewiz diff --git a/images/feature_engg.png b/images/feature_engg.png new file mode 100644 index 0000000000000000000000000000000000000000..cae057df1695d357e769262c96d3cd0203538589 GIT binary patch literal 336051 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ฟypTXuz5 z>>hBUlKJMB`_$Uvi6xPCFKLKzI>_Wg{l#E8$bj(6C(!e~BMQt|{NM6RvKI>;Fp0`~ z5>x{uqmu{sBv7UrvOT}zR6I263NbZ$(^QoBpx=8d+xvz+<11Z65YcCl!SypbQeK1f z2^3_zjMoo6b@-%$ImyttR5r%QHU3V+*6bdI*pY?vi}7Md4}<3Hlk~i}BP+OV{kK8Xn6Oc)s3gS1hYN31HZx6GTa;l(?5Kpp4)xuW9Pk5s|m!OOoT1|htXji>e&ETKU^5)Iamm=>HFTfVL7 zMb*wfJW4;p;G_yC=&2DO?hP$CiJQCLz=Db=q^17xWX1Dn zWOSjmW$TmZKFmj0dSgbK_fVTy;Smp*iiAc~(waGlcoPR#h-KV?@G^TjcnWtmVt% z8GCNkLr$_lpg+6AemuWwdU-l^@iB1dl^tecHJ17iUONp zJ8R?pKl4wH6PpsLFjq24{?(wT`p3Rcqi~*kr|PHL)w{Y4dqYj%3{(w1|Mbp+-5?*= z!Wo8}F3Ex`XHN4sGZW^5R;KUbJO)n7v@PB>$;HnsqVbOB8x;>ZqFnLBctSstE-tCD zsvdBjQ!F@tqjU4;8%1F2Rvun#|6RNt+(}e+(q8*xWniWK@Y;N*eBUtU0qQ0Hpp218d1?r}Z3N?x6nr$$C(!x?ClXKwo;)TN%AJUfvHzuHF5EBl- zd-yfU9Lx5b>^liTq!MPLJ-2_qKL}%D$6Y7wcgOpE&VF?LHx8c!6U< zz2ysr30d?bh3CjKZRN1{4 zcYT%(^+?*e*$#`k+c#J$Y5j!D|Zhh{7500Iz%6U>ghq?++;Tfh6 zS_oQ>p@#6Df_CmFf8)708s1`@aC?{*@Z;Axec-DT?ydW0?C1Gb!{K6N?$NW!ECN3miN9hnh4;oY0t8|UDcXQ#cOYr!vDq(1ONHEhX?e?@?hOrXx%v# zWPAM0Y<6kmQ8LNNos{MH=9jxK7nQ}VTD8WDBy}>u+HiNJbClqujZOq!EneRw&&`Nc3> z_P}DLKfR`0Luo9J$32XHG`3RGeF)8KZvvJQOk*~&QF6>z3V=BeUP)m!VXczx2_Pbt zVdl+8B#~c0_oW5%k~s*(KQ_K4`Rn-Ds1noQNoq(h&qBl}Cuc{EA+Q`EMQtxdz5*5M zpoO*p9~Hjk%2)@~TTgt?E|OKI!jCW!quf>5O$8|B-F%o~_h2aH!rQV%6W&iu8_n7jEX~9tKK_fLBvIMIqJ%EvauXDNp!R%U$ZKncJ4^B7HBD z{C-z2tIgl8-{~mVzdKTNkAKmGQX#K}ktL)nfKc$D z%p5$#))(C?YRX45$Jng9c~$-AJ4WZX7A2RYJAoF6$18K@OM{er zp&y>09G7_Y6lsq{cK~x0s45(u7V*(ukxH}zO^O1S#l?JK6&uOxax_)Zmyr_N&C7LC zBh~_5mRLhO~$Rj!I*g{(Oay+(>jz$&JFn$5JU;qTI_> zEwfl;^BZ!s!l`mtbSVkE3ONTqMvH%j6dC2ZiOGTJF}lX@Ly?{PkZ6Jr&A=={eIast z9=N6|&~L1fJ2oJsS+xGrq|)BXvJ^xx#HfmzpFKGu|p8=9!d(5=rM1wGzdJC}uiw%P$<(_8?4k z%+Qp;`|o%Tjckd6r%_vliK2Wo=8`_U<-2kEYrNRxcsy|arx-H7RVIS0{&gIS`+{Ih zMjFvu`q6twziU8>4Qwg;8U8TWn)Z{;`@A=+z6bfdhY(mt?&fe2!|AVSw-08s?$=-O zVUxPfD!i+J)4(8nkeR`r6;)_P66Ny!HHe6Vn79GXOOeY?5^@Wo2_3pe=pKc~hIa69 zezuveYFUAwNwY<>!iw$30jQrX!$hax`RHwXfqNfvVfLM({1C-HL|`ca-2{Ae_`Qn{ z=-3nt3~SsbU3kzf1m=BFgQB8sX=@FwLLqrZdsVa}a(rl}V1}EcB%kC2#6QV(2uoQf z2S4NCHl~P(=@b8v#~1=Ux$s#!vj=%;R|SPn@H-KRwy56q9fNh-CX8YRKs@7~G|38V$BU8b?y+J_T68r0jsN zw~PvFbh3rZ1VA>)0}@q*C-Po|hitf(SRVjvwTki~8BA-F{(Ij@-f-lwP*NlzRMj<^ z)l=^pc^k^VZXCnA1R@zu9Y!Sq&ef zqoPoJ=8e~+O_%h$R8H$TJx!Gn`Ckljz`BP{O$wWeO2NZzNrlE{&;~8?sZSfliB~m5 z?)GE$i=@*VoaC>k+-^8!Kh-^J%TdKzu;Ji_q|4Y4(#Z;Ba&KlTbFEBj7g1A=ejt5L zX*4{Aa>wZ&D!d^#tzUN^8-SS=hDsPB83wg6iv}cb_`|)RubbFX>@%WaKfPREhviXH zWJi?fDoVNID^=rvSV5RIcs^Ds9tbT&+bzl|R#NbFg(y?y;N|muanLVlHx@OG+3GK` z_{3o@>KpSGn#m{uy$vN(Nx*yK_j-a{UhWBmSI|`H-;qOjX*OU_e4QrqsWGHSB2!4j z3wyAzPCcBsc!RK@EABxlC`4b?^wuCwoH>Q+cU7PeIh02FBXZlQers*k^Q=Xs;;+ST zdn5OhfDlU{m27SB;ad8lT#uMc-a`%T&#DUU;+}oIOrKI55@+y+WTa^gWyoRY{|MZK zbHuDNR=Nb~v_|B}6?FDaRY3k!#XqDD`qUn@h)>ydhdYM~YA4Jcts-BAPp~@<|KK42 z(!_$d%jKIFd62tBIIXH8q3A`%=11$0Li$fOj6X+RLs#-BKjcJ|7<@&VR(<`KYWw2L zzOq)2=j}L7e36D@y)99`;ZaOq<&NH3wGe5Lf5V{5nQXkgwh0!pe+K)0P&OU`3#x$tX^1L_q=hVF+^VgwE0nZ z-t*`dU~Tc%!!n2Uu`g~f;kd*FHP9pqBpPXq5}^#C*>J1()*V$6 z?!&Xj?X_^z0$V&QA7r%*JeSE_IB# z^^*K^0wfD`@xS2oCc&`6pBkal8EN9xM6jSH5^78}8^!q`aFBG=m|Taj?lC-eXK0#3 z?xc^6CIxEykbuAbF#Vc{fs-iKRa4MkqLY;-N_{y#M&511p-CadAq9g_Gmc8zEP@~@ zh~i4s_+^S|DphH<79L!)8fMq8_DV*ro`0!(b;UNH8j#xbXYo1pzt}3hLKK}+7V}Wd zTG3okhFnoWqE`tJ7nI7jO)WN7n>)4g=x2goFoZ9y`1ZG-(Jk`9I zZA+piK?2AY4byKWY~eK;OUoSa^$;qpg_)h>JdUsyB)%P9%8v9PzJ?b*rWT}?v?Pk; znuqtO^4wEL>NOuT5FlM3JFOThyomx{dlvw((y>q5D>+sx5&gmj`jVhnIHB3JQgAFu z9-Q^bSG+exui+H(h928+WOU9)Il&R)ZCr0$5{N9Fu=z^}Cc&rVsUpBIE<laq29gH5RlfvjkGJMZ}}Lidl4h>xt*tPO?fW?LZ(cw6!VP@#2Z2q!JGVZ%P1G_hp`x??%J1WbU14j+{R{`fpiN zx@d$JvhDNxjA4=m)(HRVl+jK(TDe1Kn~dWqia8kc${9i#2gTdAQvGr)7kLh%vqS&r z6IQ^MQcB2W4*kxh;Z_W>{H(=xC!q!Nb_6COBlMCea$_F;^a;gkH5v zl!!QzT7dK1knNNaeb9L6xR#fsQ~#ao^C|%f?jdNK4c$Np)#nQN^-T7Pn7>Ca_ii(av8>!Q%ZWrGhCLj0HltKLqr5YdRS0~W3pcCYYQ88zh-(}op zz1P{Z;6oLGfT1Y!s)2_BnT99CL5^KnB4^W?Zy0^n!2~}XI{NztbD8A63-4Eves%tt zcPe_4YS=_4%g8ry<2r76M`ASayFjq$&S}SSbCc9eV3Z9ABj+(aVf#P}F{4iMTRNKJ68lQfO!GdA1{D^v|l z3$(TEE2^A(l(?hUH%AU_YUJ_lF)Sji3!SHrBC_KB@E;_S8)Eto5~&Y`idH%l+ffP4 zx?ZjR+?)o=0~%ZqEF1(=P_%R8f0YOsDf6tLUXi#C71%KKK9-by)%Da|_@>PKE+KOH* zvmZgJeau3Yct}xWc|~yt!-ywIih4D)zj2kP`CK;mWDCvn#GlXEj5+C@lnB676NAzc zSVH0_XjmJudIWm}PalLgbpN#$g}e`-Fn}neai~ZuKg3tSYq61!&Jp^sSzsJ$hz4c+ zegSLw3XYdfVD%@RrR3Ut1{OA^MWow9{Dt2uq3l0^Sqsr$D2)y9{_i?Si3FKkc_v`q zC|M5`15=Not^uhW)& zEX_2~Fj4fgJ*F&wk;S0zY!Yxh~y9skK5^J7fwZEf18ot3<>>S@=e%JX-=125)T zC&sLV^E6{>XI~^-zL(x>nvy|^ zOq-M60(yY`fF37LPxDoiX#F%eKWmK@kLzN$zPShlhdt=yGJY?!Nu=U9ndEl#_1J01 z%=y*bdcfnj;TZp&F%0QMaM;O(KZZla@r;h*KD@!|eUDs6H(ikt%Z_LpOmw$^;^QK&Z-WKH zt?zB*BzdqO1(Q03cyIFR3<~hIMDaToAPm!qF4p!Qh`Uk^hZ@$T+zv6H%{&<0i==ODdob=i#+`Twi z(EQJ0;P=dM8K7yjETmg{QeU%9Pvm~eATii5m|sML!g~#_h#-fOcVN24$kog>+=-$T zMR+CpmZg%k^;}CBQrH<`viBjP5pwpd_WVdu=BGm~6!tH{^R(&}qcR_Ms#&WS^2D4b ze*G(D5?TkNa0;Yu5Ecjvof1i&KLU#`D(nKkYkpTbh*mWsd2JtE$n{w&&Khjxj_jh0 z7{rI}sUbF%&;|uKO;WhM)4k9X7(O^SrGbB3G5Yu4F0*S`6ZeH9b4DX49WON9H z*Rl5#^pY#47^Ju%)U>10CQ|+)M`3Y9PfCQp?P8f640{mHjL}Cg_6)DbWP0PLr#Ihr zgx6TGcv*PazHymLc#GMoHA9oCzduL7MPn)aooN(wJ#^O|G_5NMd|w{^>zWzx`bObn zDZ@`4iRrVMaK+mJO*10@wpZ-9hkL>4^AAs(Z)f;k zPbS9AOV88y^v{}DTpuI{@4tS-nH?{^&2|8c-v%NM`F(5mO=eaPHKU$M5K3N7xR}ES z>K7Q!!Vajf=)Dr|N9LZ@cNN;|>XUA0%UrmkonSrjvUN>IBhwfP%-zkz+iO}62Z zb0axpk7-!u4@;)kFrkI}_tSOj+aGT{&Q_AI2X0QwW_FtrFFI?pUjAL{e)!|0v3{5@ zD*JbbW6^vXpKj0BDym|dTyn$VV zN`BH!fyb44n4(;(-Q+z*E$8P4eH0Mh!yVkWDo35g%wnWzHJ_+3jLrg$j27?aNC!wi zCw%8!Muv&dH!V`@k?{dMlL~FN=JR`78NaOxgw6@w&lE5E2v=)Zw7rOB$|^DrEL^Y8y7x-Dax1y?gbzLn+UA%R%#L?=x!WvrT<4udYVlzp*&}k{oyx zD!_R`d8b?<*W^g%^7Wf@ER6wrjjHRt-^DGDt!RT_zTlJL9{%?g)B=Iqw!w#A$Tcn_ zADP3kwewGd{o|o4>^)2;zMkJCC3b9O8zw$&vb{UcxcP1h)<2nPu$fHQO|I)PRk~={ zK8Rvp5iq;95p4%H|Gd^f!6bq%^hhcHBuTH%*6cM_1dZ3vr_QbO^lyJm-UG*)d(FWT z73MwXU!Cyjme`5usHtVaTq$hTjkf{?X!DzH9%{Q@)n0#^JE^)?SWyYt<`Xq4y;u5b zD^t9yQ7eN;d*Nl4-sy;{hdGDr`pp1vQitu`T^q2Fp8{-Z!{v0Qgx+S)fl|OZMBIxQOcum39sEv4qMB z0_uPV1Xc^0rb4wcTZ&}xUtkewQEo=#b&rtW)5Djtt31d5_RfgR9R#{LeEdjWx0K-h z1iWoT;it><0jhTP0B<$b<=M`qlBOq0x!W#G^AdziX;s#Zd77I`t@`k`F?31;=9ZXECG`qLQmZ*Fp?*ch#u==#c*lAE|083>7o+T9GM+ zS)HT>kDRv6Pbuktban;l0eautPsB=iKj-8)cnJ<$zC^!DAT6U#_4w$GQk>Z!CY!%<`Y6AosXy|WM zd6kcMplojlHLrQ?ClAgBxC2;{UNYl?y2b@={9Ws1el*?Byty1B-@pM#@oT;2ZE%v{ zrjkJrA;W~g+4;&m_tf`9^RrCfS~ zH($j{uj=~N`I?`_TKGmuXvTniRRqouOPm{bpP!xgbOb&gJt-1+)o9G^MbvVD`avvv zQ=iFdO|r0T9VQF|6=C)R4}&DHQKkYyb!Y*l7o$51PA z@@r0jpfI>y(HW*$kKC5tox#D@vg@q+G9~UJ7=C6=XzLJj5a62E$Dvlbjv9Y%>EhotI?4BWEtpBX6TMYicVH&O;iY;9#puD zCD7HpgjvbkA?D7bRF?%SyRspH`z(9X%Y!E!JgAk?FgdDX4ZoxXB={H+FaAx@8^iM! z-8ApBtgUiNUuwJf)|Q{mb&nNCrugix%t6sWp~ccX-ZpGvK1KX26ObMBd!D|SS$2Jl z9yuGJW5?;SoG7a{#JscMjnUYp5-PuG9%+&r(GF{`g_Jg%-F zC8uIPc4|3L5;o_-C6ao-D2at+c=5ZaLoTQ9;U3fdl)Va$ua`Hf2DK_wJH{%auekqM zzF}lN8Eq=bGzK3J@Vo3HE%!e9N^hBoii0(J8E)y%5z&dIJo`fZ6&bCQO^;y{2QZ$T zJ-@vaQk3IXdK#=u@L$Haww?Z$@r|4&)-T9VfNK=`G?(xNOsEiG;Dexs6GOYW2$wX5 zV;Tr8=`DJ@C00QNKRedff)%QZ**YYH_}ZOHL?lVZ1mUNLP^&~tywT@2`9GT&ZgI#} z)u4yT#8=qqXOVY5|6mvH)x2gA4(n-xtcJ7nviAzX3O{lT*r*-A9l>zQyNQXB$m9Cl zA`2>$Q!Dw5)Sjl|ObQo#(Wwf*z3%6C$Ovj0)M)^PT*+e~smI1L-H~VRJgmP=hdd!Q zW#F2b7<~O;SumB$>})a^fA|k4L;3+#o?$_^V;K*Q<80$mo&5&eQd~bK`KjYJ7j7zu z+}ytb?+xWbwz)2J=8|c_MH~GafhFu?)JLzKNo47x70nF@=K#&!izX$sbOwa+b3Vm_ zSJu}QEFj{#vFiDCi`7jdmAgwD!ccy9W~HUY3_3CGcCP0iW3Ug&fPOiCW!GG9_5C>) z&e{z8%eOtII=JEODkaME{?j;oaWcwQTv|p52lS9i!nwo3m|& z&qC@;`OkUPMuQ|guR26!bWikaW+O+_qLRui>+iH+NM+^Vd~lIIB!dEJiw?B1mO?Rn1V@ z6@aWe^iTu%{W4pA47XuOOHILt649(QF6zseD50|T)*u*gPpbtAeKx)B1PE{^<=x~& zRFe#p=4UBRzrpasXtY`7|2@gYW-h+`6O>G}#`{pFf%A$5kJ1r3Q4kEz8b%pUmt?-~WDsZ(z^> zF-H4tBYb(54$2wp#k=0o&EakLyT?L)v<6pIG;Jov>}|^6Sk_Bx_X^kN!+7bJm{@D& zLqEiw@|a}?ou6FoCqY*|Ox7$|R;5GV{mpR-4)&N+TQh z8IM5AgGYG}XMZR48fJKpE(3B)NRB``!1k~DF;9*y-pHSSbs+gqq5#qq$q}-A2jWtx zW~xOhW}3ALQ;e329L0c|4ao-OV{j2Zc!xC1stWWxcS<5hRt95k%WR8KHD&P^lcS`^ z1R2|ZhwBGSq zFw`h?@*^&EG{qsBc2|xPl36Nu_xsHeE=MZygD=3KG}HAwC&i`;s+r0 zDar^4_<7Sv@WEeTsTd$qzS`^_Fu7FNB`9$+62eG&Jb5<$5?PxOxJ&zfD%fKZ_Hc97 z=R)00Y9Yp?Ml<5ixxihbb4^(?uGuevnae+wp56(Zywk8<%E|Ox(FV3{5M3*k`trj6 zY^v$bMCSAlbJlIiwJXDGS7T^aKnLHh3Vl{U(3Yh(nw=nfYd1;5yK#2bWjZ$IKrF~> zSvy;LvF$gz&!?>iJ>w74vVG@_#$;wcW`#+w_DL?FuWI*Z?pkC6AZk}7S+z~m>Bkk^ zohsUfu^RHKm=B+QPw zEt;NqX$L7%gQ>Qjl5M$Z&Rfmu1--rOx52K|l8iP&X?{hju%6yz?ao7(YS3@evOM0b zxl#r6%szDdj8EiaFkgGj!rt|JeSY+pO+*LTM#G(iEIYUBMxh zkNd4np&AG|vCeymXdqf#Qe}v~$ER}QCTuAuK4Abz9n>20{5R^J`{h*gLQ2cuY$0~l z6r~h1M6YsKT5HN2@&hQ&NrnGjaU1rKMym$l+z{pMzGrexd0|$4ZuzZY{kejDKp}N( zcTiZ@qp_bu<7{wnYAWzypF>e6E~*AY)&nY++^oie=95c5vw?lRwVg=?1`=yOL|V(n zcAA4^)-q1mf-e>~J<(3%K_fD@QOEQ;pRdstZcSt_%RmN|h6VR$x@P;PD>|gH^M~G9 z1K#aCCe`10zRf(YXi}=NX?i#kK#=5;9cX^uySV}|(yGtFNFDHe5#_=r@4CT{8N&a^ z)|-bz`M+VqW*>uTh8a6CWS0@jT4Nbo)~un7B?ifsC88Po9w}cbvhQn_NM-CQ%P3^2 z$Wjs|6&32er{D7&$9ufT`@er&bKTc+p67LbZn%D_Kl$4DPnEj!F=55W=lu>K-|lpu z`wRzU>%;G_37MI>w$3h?f4AIM`RER4kMI5dd&Kz|saVxSc`&eh>-bz<7obk~!+W!B zbJP19j&BTmMBZTy1=C663b1QEQNnoSH}8>1=< zk*1wb0(0~hHV%~+^ojQh)J*)7sP|r}QqOr^vga;OJW;FoQc1LmhnkP!SsHcn&;KK# zKp4iqTv;3<4R=sUlM2ZoHADLXkF;bC%=M6n22$&Bu`GntvE=W$CjnhDRTbnf@boMb z%qh>N)IwTCy{QHD&Lkgaf>ST?L^ZLw?$)Y&KEK5woQ}+Nu_PW-rwQ|WDz1p9P1Ggp zAO6;ZslZg^!dk40rHJp_A>R(K^k&zXG$_?+2~O96oxiw?T*Omw(+soof8MnE`?UO1 zo&q+i{v$alfCw;)4J>i;mj+otC@3o?H3Dk(P;V<(?C!0+_@OO6l386(bwI|!4c|9# z?D4oo=l-QIvqg)ucLj9f%odb-jx6$LFdXG5YNbO_U*76=u4uiqsyy?Jl#bx6bRSmB zO1z|8wg=;pH{?pr~7hJWY)iEqaG7r}6EqzuIv5svkX z)zkEY0g?R9m69C3`VKB5Laf9q9<5`k6Uz*wAK|(K=Y`Y4yLpEaiPwmYxV)r%?I`7D ziM+pJwizy_XM)@&l#3|%8Piu!V_>#ZsWAZb9Hf-`q!hRY`Ek9Zks5p83;OKdja#&4 z@nAD!cyE3gi;5l^0~l z5y#$Cp<6^xA4Rt=Bx9zmIh&^E#&*l(G3ITC-_fNFUi`Wl#jX?UHPX zHc%MK3_}=Zy7RTba&r=Zp zi;^Z(k;`wKyzwnJ8vX@C|fNy{ebqlC*J)lEOkig7VMi@}dq^58i$`gHb-ez3;`gS%}<>(=vb>i+GoclQ;nrQ(>& zBqCVDx;(Efbf_PI-KGDGj0C?7I0#*#3^Djfs&#I_rmlER`P%-MjtxF`t)zg&Z8q+! z-B5p;wJKPn_;LD;$5T-}F<8fYLFdw3Q@EBJXCnXX_AuDzVcL$RgJ>1bUib^jQTUZ^ zN~*?^YXA4k6x0LCGjM7mIqedMf-6@Mr>1+gFdcFziqOggVEG?KM>Lr2su!>Dsy;(4 zn1pzc`z~lTtVuGa>8t_3BN1S2n+z``BAR-h#{r>p2*^ zaxn)DZ^TdQzN)`qPlIVmr4}zUwtU48*{6Gug}>&pClW)5rMSHKeC+_`a*4cOVv}jM z2Ek|DI+crT@iWKxo-dql0^~n023e%m;{(*i71ZSeQup(2S-{(&AG`4iNnFZ=WFuG9 z6Y}u7V)9S3Gp&_qR*rp_8xQg+1;RXGXCp* zVPU;y$YGWD(FvN!#J_NWv^Hl;7KbuJ{RHf3UujM~Idzbz)UIRSMt<*&X>U@6;8HXV>E2o+PhK$X=u-fy#XpF_cqjq#GNaMOZZpKR&%LCrgx zYR|ic+7+r#eytjfNzkJmr1flMrZ7z>+$CzbEPZEs@zh^*?7jWFv1z6%NKJIScvc(^ zY!9`rlT6Y`e8jdy(J6pNTz{~jz@C*9xhURxGuwK2sudzqV7*vup`v~h2-zvlG0pEt zCHS>);u}JcyL&J1%TAaL!HT3n{#p$o*9qJ-FjBY^6%Q#@)?hwTAn(}}or61x@SArM z{wG!CUK3;JA%%MXjSqE$KWkL{vEiVSVDt-o@+>_58&DX(zI`Fb0G{&I)6A#!vQU?r zTlAu{bA4(I@i;tx>rBJBsX)C~nyM~9Dq z^z)AAS)j~LrRBHgjiX1dVC2Yhhw;@FP197zN`-dDK<^3I^KQM;e=Vlk8~512#;?2W zUc^lqAl(kO1E%UHy76nf^pEPl1eB{tMtwc0AH013aIa7!YAB$XFRFklAbK z<#hr$LmLgi6!P5Tvon6gdQe6wuqaV2(XB+T`Fk*tmfrBG*PatsXY+FoP}DWSL^^9piH zButFgi0dq4yneYNkoHu#=kBW&)9mL)(t9F9zs6-s=>i@5^KW8g8Kn7&kci^FSe z`)QQyNeAF%Ea;vZI6Zlp1ixRkA5UE+?aL_(WPWV`W19-aHywt~B6=@z3^O&(tArsi z%&weUY&BMpt8vGV2`*aLa7+e?{dqORI4p?KUaf{8A}`OlrRX{r4P8|hc^JpOmYjpj zc|~V`8!Ia)Bb!=+nPS9Fb3QArzPB7sX-#~sF5FyPGy&C9(aQ-g%9c%)sv^B)4Y17E z*&;xePW>d9gE(U>(;HU-)FcC(d9rG${Ku#E6o!|6~>R7DM z-a2sO#N>7(P0odGdTxT+-Amh%pPXyFns&Zi7Rb>QHsGUf$5X%2AAeo(&UwqfA&2?1 zep`I!&Kb3rVepAZTDlMl@&`#pM!fHs`Iqek;V{T~O?^h(1}bbA$Iw58+F0yo)=O~x zaX)hz@sh|btsoVwJnBp=ha`}#ZE(Sl$5O4qfFF@&Z7l&{zDxoo(MV#Z zjpc^A%wQ^HxjIE#Q#nvE=%&Yaq27x<3TG(OvXd?Y!eNw}I2VaCQ^TS0KgfM^p3P)M z-mb@#U$5o@?txuq0|G#X`WaOfTGeiS_C-C2!I155Qe5ul*hmuE40#4?(7=tK!@LI% zUsc=a-02GGt4Ub+^+M^-{`JO)m(R679q#1VH7P!P%nFho`$SoRSP}#VUTp?<{zsRd zhlee8FtYezVN7>pzOs*2W;vfJ?wU(017$}mu5whQ7Y)m#zOaRyc7e#;%5=0qXknd< zk3`=^3k&iH=0eQPtyK+Rf>gk*peRat{xVq_xT<{p9~%d7>=fF+C|FuI_JI5ymOQFq z>mU{^gqZ}8eS_%JwQL-7$50?2u%=2BZ>w7%pgE($EbGu4BA=h6FH9_)Tz%(&YazC#B{@ zsk0Gt4l)+AhgrlOOWk^h-0_3*(+BDO9@akWvd`#Eb~ly0Q;UOBJJ; zw*e4d2@Lqo*EleDLaKo4cB$cYdI~4Fo4^hp5HC_J#bikpskC_O3hfX<7FrK2XhB(# zu~-Iui9rE|KJ#=ZvhV``ZX(f=cny~qkgt7OnJ$vIBIb}}s}u}#yD-rtho4D3e)9k= z#gO@w*&CC91{gDcujTe~%Tp~@Pwb+R2PQVit;HSrsxTl8Ywe2~5r$W`LsF&W8pWQe zW5P*eCjV3H2~YAbf|DcFMilnQg=2YJ2m342(n>l9`MZw<*X3Ww^e~AbB)VQ zLRAecnx*{Kd%I3UWaj+k2k+N>jXh_3DD21ZJDLDmD_wb*;|0{(rb3BmBGH1ryWy|~ zoG&qTj+_t#l!Em@M*$3QS)pX*pVqNEp=94Ra_OtQcqWj79_OHjtmqv9iH^~+d;(Hj z+$Yo2Yf`?d!JP_%)7UsMg(0nlIvjJLU(ZOhIQy?+v>zl%A%M`*|2Q?m2^wsuW<25e zdF~N)z(4dbI9+0rM+=;4y~Hr~C0swvdG_Dwr*_Z2HRXxyq{6^PC?zq@itkKwl`%g4 z){(dywv^{*6+)uEZ#T|-K{YQFSk|KB-G_eUW3G-!z~{hrXrX~iWr-C2=1Au zb%^E-i)}eO!5oLDjtbBl+QLzEmW7AMdci_;4{iEl%7N`CfDKz?TVelUnsXJ2R zmS%5|Uh2qDalSO$laBVBRYtJ(+yTK?atxV#yo&d)@a5_9=MHZJfTLIM7dWsse9xbk z()w+L)A`TnS7ftnEDaW{UQ4=EOL|o628bN37B1~{t+&@U{{3_+ZWrYs@-ubl=+Cil zInK*E`V~ggjk>jZFavkeVBHgp3df^M$f5!jVeOyAjH5@?g(c!4KFAHmV3U%ujnrt{ zNp_x{9f?1hqtEV~{N8T&;JjUBj||d0Z##CWRL0bvOCg%8C_Z0M%L`$+JnTP!o#&df zD!K+L%s$K^4C@%6hk|cgr!nlD^yAaSHjV_Vds02iR4?*h#qxvr#k3vDXpbuu!TP>C zy)8u?AE&G$)uPJS7OpQzf6&hYz%PAj?-N%3Q#F7VfR8k3cgJMuTAtPX?c)KFYLAYc6h;Jv!Yk^_0j zg%-z&IQTTA;9}X&ZP!Ig5~rm!D6KqO?9B2UwGp%ExP`t8$-%FhM^?<`%y z#(@Zz(KGnu1$g|jY~n%+xc?r_GHN!bI}u;Pi93CVRWF-z*hQLivX(M1+6x z{rolFNhlM{u7%g8B;Fc!KkcaCAGNj}(6+Pp{Xpn};w{lqXN!ppRp6jq1j?VGQj-6v zEHUFO{itZ(cFsMlA1Tuy*%aMheM5Mde-b<#qK2DjqQGZ>yW-Q5CnQXwKL6exTCJ43 z!Xe%z!F)xz$<|IpE?c|GRs?#)-LN#mY~msOWBN-asAVwgH2o>fcazV-=SoQY)K0Jj0m z=d90+44zu|FEuAFF5nBG9dGGaMOBRqvmpYpH_yW;kOJ*Q67ESM$Z+Gq=jqT1LcXv2 z&5U>U(r*czewbqUGb5}X^ZP&0l2`LXgg_lgbJke=v0!9?`)hEzv<+ehx$7Xcx$q_W z6LL?Sp-$B4vHq|aNMUQsN|G?FA!PcbhwA4E<*A@XloRrA=QX3vE@FIjjfQ~WaI-PGdVDL##E$zg~wxW;y83+pbi(K5W_#bXHs z{fD2Km$7aMl0W~hiPt277<(7oqm&&-fJiQvgC<&mUZ15GfK3F&Ue27wBj3Rdb^+Yt zooluyog2#>v>rC*zzCaWK}sfb$|&&UI|J*Yo=u|07bFsPrPdy&eB`BUWTG}2X>Yrw zMRt`B{DW+PG7zbJOqgHCe5ET;Dnl2=%&uH@L=g zpjGMbrCICQFTY1m??117IE#xm2Jj0-u&|Salb1o;lnIyfeegk-`)h#ctW(EO73A2( z#WgTxw5M+ARD5!f+Q`Md`jd0v@4}zhM)8)f`0RnYQ!vxmr4%e%M~@ieOd_1SoE90% zmidZnc;!4>Atj(mNQ)kKfgvu6;_TM7Zeio;zfobQ{6_Q4=vo`c`~CM{77|hvhN0Ki zzVn0y6a=b)R!i7zu3F{e$hr~=DO^RZ`Eyz{$fsxH6(pura97ZG3{VW}saJAKeB_vJ z@&G6N(55BDq0G<~N{fHaE~oKb@!u>e+FzWNjkznQSEu(nSnP&ISgRH>_MR6JBpQL` zcjFf`{*ZT$jVQk%1Qzt7sMe)milGy`qt zdc+q#jb9tQ3Gd`BThICg`;?rhHte3}px|4|OTjIr+E%BbPA0Qy?VYolZEWS4@)D-% zwHHDWF~Ofoy$h~R(Hlwjyfx3Cz6}e_Zc50S$bRF;?7U~nJW+b`gTOS7 zP7P(#e8sW0bpB9bjL=MT0ps2C#lS<5q$SeKN7`+76Pvoc$a6@Ws40Jksq>Exo~=9dxD?X4iWu*==UE;#yDs~#R?>QvKv>x_Owh6xIA#5uoToQ| zlM>2t6mUN)A#hBE+bE4zlR`gk=u!}sX3UAH44GjS4W~TNZ?EPTh$D5NGyjT4O`~@m zIK9#uu>;$+1)`d8{k-=nD1TC!wEA0NUNmO`Z?8;0TU-A`y7eVw3%G?$pDniv;SQOi z|Kilc8<&goaqwaxC@XrsOLIvZifHOdZg&OY+rf6~)s3`movee;S0e{%5Yv~gKAR8d z2G>P7Mx6aG`Y1M^kYC<+d=jAk!Y5KBsPlB)%?B1Vn+>b8P~`!-aOqPJvPUL%9~!*x zu24(5Tz<{{}u4j&?M1!EE9CTQUh)l@g zgqX>KTYO>fr#aN$aqw>B9&hT4qUqo6DLyRBIjn2_4Qw-bL&`}R)Rk5F3z&^-{{H+N z-W@qHK7Q2Hh^G@(Op;B#Zx4Cy0g-9xL>_8FDwOdupVKGogI@;qd&NuHVV(G$?m#vz zty?W%PW}mB20GPBFSHZ`dlv@6eN?e(pZS2zvJE1U$Fd4qCnR4>5#|#Bei$xf6-;@0 zu;@-n05%1*bQwL5PhNsUe**OeRb>2(bZc1hhs@0~7!_?d3_7=N!mSMXKI&e6$kg%B z~qa@m3Tu%mB7yOZqZ9|KXf3@N*57O7BGQdGykQUK)rd@E7C;&3)n6-FA@a z!B-pdwMAh-=YNgF`Mo-Y6-@CZqFWD}Qym`C+vWf{8bPyMzwLzy*L)LLM~nHYt?|rd z8A1ZUBY_0Uk;=TcVzN?&k(OczCHQ@|V4>U1JMztr3VgR6v+5M=T~Ln!NK_K3r#BG5}xKv>{+A)qT};L^3-!x-=zA?Bxuh@J(>(% zUL|u?A9+Q%DND9~?Mun7gIt)#p8m>)uIpkGf+~-~>MH?R5ObKhn7UKx_Q|$r3eGAj zen6b}@EC}y)TuHn>v%5nF=?Bl4Diw{1Gw;E4ileN-Ojdh>bEsdZ&O}d|LzR+;99y& zPJO-X3QaWiY>mdbzksTG?pItrC0=_gQx%d@4Wb-nl*M{e>8qV|e3M>`^TjLaTnONs@m# zfEX;7AdHwzkf@2L;~K$sVi|T~t=YGIx@#|W)$Ei;H)?L-Sraj^U!+MEtQ9?p9otR+ zbD;bpq|nU(y=EidbRWJL(Q+Bu)eINT6kQ}Ch?4L`NZYjy)gI3K>HYMCH}g{YZ#M#$ zlivOwDR{*mUQuw#&uB5rzfGbE;M_!Al8WhC6jjgIelY$qDB^ zs#1lU_+N4sUI2YwpodUB^E@Y2#VVZK>+H@tz$j1;npJ^BI1JngQa(w{rXyu;s9VW^ z=T4%OeZcuD=e%|Gf1{M^K)(Mf$Xp}wBxsPQ3KB){k}VDYbcC5%xhN5pZ)3 zw|zINQzVdV?;-C~z~4-ZD+pfI5EX&=U-#00Sy~q^-}Sy5C;67-2p*@v9)?oto_8lpp*{J`(K<+d zG_-;n--#KJ!@Tm9ad+*RoC|nf9whqb9edYU6!JcKEL}#wgYt@ zE%ua6)pMYonPoF=kQ)ZvamJs>ZUr!1UKN z{=<30B&b3?SV0mb+g}D7Mkfq|e0-%-`04#;>wfEUexg&RZHyZecjPxffOa+Vy^m4t zA<$sYT8hq#0cs$nUp0H^@L+j)C0o){nn?+x?8VBJWLd!;v+0#z_vG{%uyrDW(;{h# zj?Jm&#cZXXyZ+rkrDzWRq5t{QjGnD)P|+u<@8$zCO<%$w?(&S3=IyFT#fJgVdQD>D zsw=y&z{(9mhok#zUCvZ7MWFO!dgU;azH5*7wGwu^zE;B?9RB)hxT_FWc{S!kq8<6t zbo_pXmBrbTkr2{GYCyt`QSS+W);s=SF0ATD#@j2~ zDoYM*vKi@PfPQV>Ko#v?eD&ylJf5kvzmUp^5l{>JNtWUZoHqB36>oMfT9vI9%ilsON`- z7~bzAfc;Mq=T7y$&yRnh8WcvMIsfC`UhM0z{daZ;9l^l7Zt9uqjaVYy6Ok&X{)?k@ zZ-PfsJ(|mF0BWo%1up@}gf!>B{8mi!=?18$n9}OD1zr$Z5mP_mHvhph*4%-P7Mbz0 zh_L|Gf@f>N*o^P(u5>r$qMUL_XG?D6-kVOh)mWR8*Ihn!9wc|q{+v7@n;vDS3+cWt zTGq;mzQ&M;!xEQi98;kB8i@Px=eW2&-9XZ(9tM(v0n*m9>s*pTi0F+?2&NbEcl>QYZ{9+OJvCK94Z$dOA7` z*mlBNAC-vv&Y0n;Pv`;L5167O1}q|^porKbfgG8}yf*R?rKPg{b}FWFw|(?6CyRl(%Eq_cBwk65;eIUwx0xa2bjrmG#h z;jVx1R+jQsAvp)r>`m9iU)t~3ehn3x=?FEs(5+?P@9YZzXx%0N!Z>dS9Q_ynEmqRB zUA}8(dw>@?5xXOtclLR=@{-tZgRCqX$`N@UkMu6Pb1=<8biaVr^s8=O0NNC*^tJ_j z+Z$cz_S=kgP78E7ojjMk2rrluSlfeWDr1@i5&Mj=s zEZZ|0l`>N$=jRQ?Z<>fRVr%)&Xih8Sg?PMa`&yQ+y}cZ;@%CYRm3D<=@lA3nK*XP> zipcbK8jP<1NX51=H7&KIT#gwlfA#v9qVsN0`%lkqa4daLG8Lzi`ZJptbm2E2JzSFi z8vz-gA}l7LAfUxX_MGZZ7SeCD=p`G&kuw@Gtik}ThaRtN0wZ@KWMw9g;zLra{&CMgZJijROsxM1&Z=#7#_%n)8m z$dHrxSQkOZW1LlLuo~lmB{h~pSuWIJ?@eoT=S<_iP(X@*Uwcl;?Efjb-}G%R&kSnZ zrgYgAoj@42@6Q8ZmL%SM?8LARx?duz6ffZbb3e=V7#4ANXKz2mIsfSQ$d^fP+W|Z? z&Fr-Sau&G&FL))ey2r+=j3Ejj?pC5ZY(S6vas^=MToB@+C}$p9qWuCTVmBq{LLlsb- zQQh#xJ;h(~{?>iDH6#u+V9iJ~yaya#vTG6;yLu%lt;}e_~OjA4qbhxg4`R_*<)xvxi=H>xP?CpJzf$d5@nLRu!^(#%{ z+slOhrT({K2~FGQK-xss@*BnN=6rHJWVVfu&$=Vw{JQ*F*%)R4k(ETJF>r-8h^tS*6Zsbs zQT^R$JBu5TYpLXd3;5-9#zuc_UZUipZh@l;Z5fk z{~Zvzc6($_F7v%T(_t9R>Zvk}fE;Yli~X zTA@wuRv<(Ar3VXQ4EJvNdshMvatCU-kifxO(GV55nHDEZ;U07bK`p z=}&U6%>z^z07bn>k@6Pw6hRQM!$GOawP5RXR%&-*$!r07%65x}2!!)_|usXTJm_K^m55FK~=Bg{JD;aN$tF9>^V^#U?Kzzb_*Y zMOplTNQ}PMpOVN<6}SiC=z3tms^pXB%@+p`Jq{g>)o4~-MI|of>MGaK$!+MHt1hkH z8-ymBlw+$gdEz{tI)s1X`eCO&N^?FM_;A6Bn)mc6c7x{`MYN#52Ie z>VL7uC$GVwd%%9}TVyP!JAkAfmbEQsPaYpp=McQ%c*wJbwN<}7%7~c51Me_x7`+4V z=esJqgk80r7%lbIsOxMVdWOURwJYtFzwXI{dwXi0BKb*zN+PE3<9@! ztM;%{*H@D%Nc@c4MGjfP^onV~6H{GPTU9S<{U$)2IKqAh4e(pUT;ih`@R*}j|F2>T zVh_`E=>VJckPYOyNia@pl^DJlxV6+IV~1+Td;B_xlSshvhNTdu%tD zr>t)FdZHHfc=;#I*v2&adgAVhFUlZ@L|+E826*u55a*wh)+*>6Yh`;x?)?^zzL$sY zMw->b7k)pDd!?hjU{~ofWVJ*fo7B-m+t9cVE)Oe5=%Go#8{nG?R`Lw6V0j7kKDKok zu+7p94?BJUh!;xqdXJ!PDMzw5WxYK8fV_ywz}yDB?MRX#V(l!9;}-#$lp-u8a7;i~ zh1)*uXH81b2{@%7HH}fHJJMimeHZ(NMq#ZoTsV^>Rs5h$iG&(e(AV1yEk(uj1i5Lp z*?*ONgqyN)lXIsu5ZGdrBDQ@0_bW7L?*k5}Qrpx%QfvCl7^9#+A^H3G$D83<-|kh0 z0NR@i-Qx|>mw)to)Bs%40a}qkrQ|a=nJ|$Wa|@}OOR0YZbyd)2o7+lyDdcj2GGWAx zEOBxbs;m|zNj$dobX-$SGyQX{8pnIF=-k&KOQ*iPZhCRyZ2X%-vxEb5^qk5_S<`>u zpVz@pR5KXTyWlNoB^Fo};U--CUU>f0=$;hjA>X%drgDbw85XqD2;O#`F5_n$sLT(U zrGLV{Z6}4we*57Mpi7aTJI$+KX8@BoDB&@ix%h*0lq$|WKl`&yJzCgF)X8l^M(yH# z6z~Nsw1x1zDkv~NHQPw5DpD3G4FHXy(;21gAoiYWU0MR*D}Z?m=cN<_dosqAgLQzz z&qo&!yZAxo$Mj1x$@21juu}NHq@}Mfw$qS!cxa9g+9e%d| zb)i|t!JjZBMIxaY$GCzEEb{>kcJReXpH!rCqt`=@X!30dL=jY- ziXoe4tXt=h`N04eK>4i&ytqpN(Zzf6CLTZzNico@_V4_$w;}HoIkwVWp@tMVa$Y0V zL99v$^95*>$XEh9j4V-9Uxvw*DmoQ4f0mi>-M7M;_BCD}cu8RgiG zEYQk({91#t%Bff2X(ZRDv;q5a$9g9ie_NwFK&*m7JO4YlbTt@&OfP=ih3<)%(ki2W zg;%)j$tCf}3n9Rm;dd#;HQ=5!&Wqi8rm2}558G4|EZ$99irfcwZMj~;VmptXN^h&~ zhXBKG4J7z)DEFD%r*r_&`sZ^+jOs~{Tx&J8`mAQTJJFT!U1Q}d(0Vu3O%~4^2K0{ z`ICuB3j-7!c13CqKi?KOcYgkI@c0QI{%VI{ay??R>RBpSH39|07 zpZ@+HzZgMo^_T9y&`*uCc+0t3!2h|`yTj<6G;c=+_^9dy2Aj7D#mHQMpC8028LR6^ zr_YI4WQ|qbPBy7A&Uu)(vAHI=RE4GVOqP3pe)I?EZg{*&$2P-3L47sk_xz@ieI7WO zVB+ZYiBxQV;c7Yn9vxXtTm?46VnEBNYEGUAl%S5%y@2nV4X`Lm=3-CGcp?(P54sda zfwmwCuUT*>#peYOMv!`yJ^X%^%(G5<28Ci&z>+4_2j>ns_v*9%jMSZAtgf$!TACPg_=YM-Mn{K<`8@S zd=z`$G)0qU3$7>o@db#T<5~C zz-hoT=pHw?lA#MUv7ZwFH3eR}4Z$8|Z({u9)3q{Np*&?gRnN~}bTT+=p8iCcvogY^ z--|X}0J#ENdI%Cf1*|AiGB@n4PJrjkQT{$)ZdJevQaqfuHXbu5Sadf7_o7xHjcu7e zP4|8tjb(iPHq!lJTg6iA;mop5vr()1IyyaFYFn)Dr%iu$D)u5$LR3YUe!DbTg)4^` z+gbBeMSJv@3(QgEn2bEEU35Ux?k16|=KKIo;qRrnCl^1a?rj+T@n!8PMkYX+-Oza6 zJrOexi?9KUmnMtl^1EQD!blx4gcQ6Q6|Z=Vu!rZDQTrHj-KpcFen1EvAJi@#b@QOt zP=P1_8}WWk+B9T03w2`X3#!fBUWV%*p%s&f3L4Zsww&_AOa1-vc4o~Gj~etK=;cKyY6d zCzqqjs!)=OXZqDn#~q;FY&tOSMU_>bgnYC~l01Gg^jh|X`P)x-IEG{-4M0)WjIcqJ z!6hg|+gl-GMgrUwUPqaM5=S`s@8bCx=X!2_Oj6fT_Qylmo0tRivlZGosjWTE+gEbY$tw60X~O|iqUiMZj;$74^9^AChrPRBUr&cNdUF~B ztDH8~Y<=wrLvKvw4KE!5c$$ij(#1}&I6o}`Q<-GyOFlViFp*Fa8CdL>Ybg4 za=;s?;F~ofPR`)eN*XusoFk%C5JL^a$^~P!sss^|K zsVHlMu6skfzCQ(Mn`!57ATx{}lsxwQ75;l8{JQ$D+3b)?%aZg;P}xk_j__VWl%tp; zmVfLgt(^;97knLV>95|@H-t#Eg29D)NUOxpC@G6m{U)hTYx#%i){epjB94wh%d1uk z{Xec9c<aimt84mufl{r1(o|IP3O{<&erkk3Hi|?j-QEW^y&v7IY&gH^sXL!Z!F{~SsIXJ z{TdJr7!rNH6rsh6Xded<(IJ`tS47QKSP`w38sAi%|F-XaZn&rjH)A?E54_>hm8uKl zPnelkf;nDtZC?N5{pzHX?G^KSw$g0s@B4tElG) zB`y6Gl>ND?Ppj01szaCAAhI;NHzTl~k@XoVZSB2eJAkuKAEbMHoxoSi*wg2pyC3oT zSxG2QJkt|as+;pnab!Z0p~uorma-prh2@7Pe0ks5`6+Xpm<7-t@yxu9iPN-%yS|C| zMF6QVP7@pBQefyU0HuNHzR0iSli22$> z!~@~H60wnW)v0eQ-qy^fe8-CdsjEIks|zQg>T>}O04KT;C+~2|@n!_z)Dgr%bSmg; z?|TZpQw0`zumpRuuLbBYa8I9T*-KIcd1XwYCsojt3(=5M8;7Xtt+b@cLo;{?y-yiU ztdb+{BNLAz!W@XVGik%*~!hu(zZ@G zv4R?6$%S|EkBEAbS)!%k%k=me!nChDXJ(H&d6l3C>Hi`;R{06=IF(6wqzc04v%Y4b zPxKfT*W;B6J@E%Z zyNQQ6CV&>bifBsvjX+RWC^S2Yj3AMw@u(}7ed@Nlzj6Uy;xgr?6s`df_`ILC6un#Z zSYaA>P3{HIO1>9xI@w+F%mh{(U!Xawm^Z=XnfK+dT8=B8-;Fkm1-8*xgWgrHTcqD% ziHnv9zs-*p~caOjmZ)glwTM@tk92(V97FWTJ z#Fjq=HTxzl<htb# z3+;!O2Oc|zDTJ2`|IoqSp|4}m-w(0AWU`MY;1qJ3vS(ZL%6{Y5^KK1(Wul#!ZT3f( zKV<%T62;%!YffpT*op%dF9{A6P~2)bN!$LxX4+{KS2>LebGo-k$>8N|L2(*G5`$og zkk zP5Q22e@Ohg8tD{E;j999s>Ibo#ZJ zEj#m2y{q1JM1~A*D+`%^p_S1F=#Kp$Z#x`fD_?Vld7N5g03qx|aauqURS{REiJh7} zmBfm6_keJgOY{0rJ5nL9yaCw9m42qI#mACg5{`sxM#1h<4cZ>Ni=}J=^9|XNGtkp2C^H z+gsI3YvJ?(%uQ1CMgbp7gqIW$M$2tVw~7hPnjmjwDDSj+f@AC9Xd16lSJd+K0L#GM z4?zD$_5U~XkS(N*715GH=9vcRrubx=vLv=}08Z;{l9YZFf>sbjVOinY^8Kzp0sk zL3TQh4BT_MAWS2V{sitfQ2lYhHFciw3syS_{9ij@ofwU3gLvJwE$E%d7i{9%M&D7; z2#EVNfL;u;YDG0=*$#lxe0r(Y1#BG5+OVns_O-&^b@7#EQU4;=lzkh>yqC3%Z-*$? zjsU0XX)m8dd9p(FRb@y$Hi&5;dWPRWRb5s2JeS*5kJs`x5n@N2-cRi%XTE%4GT`=Np^; zn`;b(UDC~;|}aoYkQC(6mBAxY*|c1bHY?*}k$$7N?v2kW#e*e)LDbN*rDs#lVf zBi2dM$$P>PPqxV{ibmuEpzhk$X`BTSQVrwxO|q$)JeN#Amhn3 zN1B&;bV8*C(3PJK`Jq+EN^Y*93`+8X}Q#Ca-R*w<)hmRIB62 zr7&CxuvF{T9&sS!MvSc~Tnb~MMUOr#o$?1@40brHZ%3Io$;vbPinJr&<^x46iH>#6e_>mK6P>f2rE}gFO zd<3_so%8bbSWAJOSlqUCQL2T4VLkATKV*I5)$P1$Cy-n3MV|ks^O&;Qa>$f!k^gwT004b=AaPl!RZ!s^4MCoHco_7PvIJfJqt}|pP_c89p(>k>4*fp2K z4DnsTEmf^J5x*YYk8J4Q#*D=}262os6LRJAirdXUce?<2VMU*pCsENX{K4yOCQ4qf)BvJ5S6f0I#j1WqEv$yTd}Trt2cF}m zQL_Wt4KCaOdQQn0KQCS1SU(~}Y>_okXBJ4M9jg$;1Su+W4`tQmgkWpbSc&H#HYkMo z19b7VI07=T6yoPCd%1#L!YC9tCB8pb+;c=M8YQ1%H5_>({{NxsyW^?;3bv_}J#B+1CSXejI2WZZiRg`yHFGqN(Sy{~(bRI=mBx@6C5UgNIc%jf%j{CJQtWGdQx;v;Km-i5hzKPou(!UuO&-N#o-1Zq6p9O0Gw zQi@M%fXCpHK_l$^HC|@{nG_~|z6kYH8Gn6c%GjMhT(OE8Wqe>ql{A|spMW<#OK;E7Phl`MRJYn~)WlFR%87@M_Ug3!vP|MM!=6({yznq0NpXopE1?#Zp)aHq~ zKl89WN7g@L?OXA$@L>1(bV8_7xMT&@Gt- z&{*HrY~}E*V3KOo=h*~%jAEKc4Ubx!=W~tCEIbR=v)61RT+n(=s%!uW_5Qy%@4C~; zcNb5wuRt4B&nf~kZCZ2sT?MU`2E#7g97jVes31$!>bYK_Wi}jI)Xcu|M!$6t`GYj1 z9GY;z1%H4TQrrQ2)U8wdunn6Mps+KbZoZQBNjCidr5%xLJa@#{&YsAV$d&vqQ$CkY zeN~#$`}4!+I#x3eTxr~7m~a(zL6WC>9M29fb*R~bHH?K{9ppS5HQDSr^QOyz!!z7B zETN3aB{!Sx+CC_5a+(2JDah9bUPBuuPCxPLKkY&)4QyhU?_CEqp~EQ=qabz9+nPFz z|CdxI#a=!;Z-R~#Ni+*ox^hwPM7LKcn)@oB%)Y0?lk0k#MxW0c>%|>(YG1mY%y}j6 zUwzke93RYo{yMrWMS;^~Z#Yj#E3j$|r#K{KDQ3rNpvUic%JI#84e(NzmEcG31t2RtU<$e^Eg|5j|wxq)Yr4GVj4&K)2Rj-2e!7vAoH_Dlkfmu z;3J38k9b9_xie7}r%JP}n@^TbtZl=X9$Z&>qF#C=NXhe)Z zyLe5F)oJ`-R=f6;?{w=|=w$-mbFWvwG<1b~t)J_qPX)2GG(tIeKxU9U=ul3%9~{pg zy&^1D=>5}0XRe%bz0Nnx6o_yNgnnoG%G4dxt&R?TcwY|X)f-di&$txU`u*w>ei9)W zf9jP=n^Xp+MYdaQ>HP(bjIrI!(GAHO?5?!3&PHs8b(w#dM8C)q$L}MNpKgD68t1}C zU(7Y4Wdtu6F(gcs8qRn9)(Yz{T78NWjrrM$KCJ0U-bS; z&=)uPmW9y>V+xh@>`yJEu8yG?v_W@kK8?;TMFz*1GD>jeAhfy{GNxx3ZMtd6tDAl1 zu0ltY@MptMd{X=RXJbz)ehoFh*2-;eY7n}`+j55m#{c$1Rj_x*=NID3sLh;YyN7&3 ze}+k>@ZCm~hy-Sh4BGsnC0EC%zUVgUheFJ`P<1acnVD1sSck! zdrG8UP-Gg&*TbCeD67e9_FP3rYX1o{Em_iQCUB(&UlV_Fb~r3{{QcRLTLJaE|1jk7 zYfnBs$<;6H%)Bh$(*tDlx}kb`utQOxH+3?|vH&g_YRF6Z^nQ|U+HJNG*gh`=e=Aa)g*D4A{`ifH&DVg;6(=UYnDzJ<=F z4gOd53MpcJKzZh8xBBRR+AaRDV+Zod?_sYF73irJi#*-~RDEdYR1tO*G`%3tHUmnTujn9c?<9G;WjJo^QkWqok zS4RGfjn|lkTNDS&wNHtBFpIc-qXGJDLu{B;jWa0gHo7j~e=E z^uqJKfbo-3(_b)Y`5xaKx~`^ek?^hTr964MzpgyA0;3=?k(rWHmPP`7*NbXim--54 zF3;tve2ebUfAQ->ZZ5@+XJQ_o1KF9vyY+KaZgWWC6Rgoh2vi6Z>Ici)>U+Mz4ws!3 zUqnA_W%GQ_Bz4PWU&x_3Z?qrj8-q$k2Dj2NqTq8cw+_7ohtPB4lYW)99-1ZJ7P9DE z*20!nbW+|!^A5cG7C_QYbhNN=+lPWFQAKby%OhO4b_p*ybuA(-Yd5tT94CW`s5cVQ zKf@9r5+U=NV~UO!9=w-Vkr!~0bc|DPIb6g)p%vU8(sh3HCg#v~ixa1@g5Co=I-s2q zLg#x!J1iCT^lvBD=TAgit(tRtW?nt@)xO+uvs`yCfw5A`-Kf0v;jI#bERAnfU>v(b zO6K9wyCdfmAtTFuiZ9J_AJ`O}>I384L8L6IVXCy1YdyXh!78?v3&bu9RL*QU;;sY$ zcn%@UzGh^<@h+Iu(7>_tlFpkSE*1T44exU-Op`s7LuJ}Mi8tJCOwWs1Tdk+qVa@j+ zr=f(jk25WbJyR;^T7?)x0=WUaLmX>$ zp&V;>l3NyK@URwx7PeBbM&q+#3aShR>4OEZ7D-Q5(6a88GfBxA@SNsX4?p+mT>H}nl(+$okcNUlMUVamJ)wk_D-z}VGzR?&n zCiJ0rKHiDMrIwl?pJS;kyT6vYC>;UVi_~R(<)98521P$P&HmP_m;q6Vx_@7N#%mc% zRBia)R|YKtBVM)qmogr_rb$(tvt1jzm~z~hn7eWW5wh|tn$<*elh{{ND06(vCwOJH zeLP?dzCRMLzRFg>;;O23<`#YXrk6AJn{{=KHxv%P)kJyZp zQMMcX5;=?K(Gd9Nk`ZxlwdMtUCrcQ+?ar9Xu}q!$>ZtTym%3g5=5d={DKWsiC|ipeo;{eOJ{?alF+1?~VzhhDNN+V8Ag!+ZpMh<2%W^o24%c zaWmnu-JUsGL!{gl`6kr<>3#ZKDrz{!S;BNjx4$ap>s#u45$P8(O_M}04|rNllt`m# zGf*>xlKVK|P@t1?e3QUf+1*Y_N`^JAz1<@rO?1K$!4&vt#e@Yx>MDi0JjZBb%*(@R zSs_cwsG1dP9}tNXMRh%Qo4>LmcGuPO_B1z_*S(%rar5ZcwiUtjW>fU;Oi=xERWg0|LbKl=u*^}JAoAcB4etF z(xg$0vG?DTUf}wP?7vbbvJsb-5?T^U576YM;6KyJ{3iR0z$ZAd$=Jq^VP|vK0{O4N zkVt9TiO9gt>$8gcPF&^IMPIY;*bp>&1(Dj)?7~ghGF)fkJw}=qYB6Fd^p2XQ?M@)! zY?GaDwf(z4gEqkvgkN32vIbNTE;53L}VRIr?Zsie@`?BS6fzv&z!dokEj9E-@%Nl;w)Y zD~sKss}&pDP=t=dj*lkIgrQlD~hAg9%}qDo5J%r)Jb;^ zB1I~g+V3XCq_hgOYJfgsaLn5K2m;Sh+A|RL8Up0M-4o7%GIiRZ!W-K6*V zCcx(_x(;U>%XtnV=*$j{TGpOdTWRpn<+(nYfC@*aj9Rfon`LT)% z*lV-HmXlBcy0|Jby*!l4FELX6;6Y4)roVd-@T)yVz2z>O8raDFas}w%cfivE`_^xG z4dj$waZ99HOW_&48Ne+> z5|tXJ*#2|iVszHyB?v9l2BI#}cg}=5CF{R3K55=w_||@5U<8R6B8ijoZ&$uG=3FB& z?x0YA>FW}@lQav^%yzGKGPLunv`+8!PH#5A*JIWUJg}iFuCn@nQ*u~x8jR((2jn{3 zW#jAp(^EzlxMy@O!l)GU?jlbNA)-uCYBC!ftOq7qucA*-$pPJCf&JSE{RkrBX_lMO!9@wK8D?7h!)-p%_* zT>GRCDK$`KJtx{TLaZ1YOx!_~mKUpL0Yln*BP(=)#DhZle7Dl3G8wyorlCB8=I^{f ztuYlGmx0Yn+)hB!zYdd)Eh2!Vp+e*t0VCqpagi!DD^| z#+BnyxVgfe(3w49`jfHmf)?l;yT{O`$=SkjT6${aIC8?KWVfxYY4bZ>)IKpOx6v0g zo%G+bGp-B6C5a|(V|&Yax|{K|KjFx`ASlTS4?>^-yF9>?h}8>R0bRJ4H|D^KSCr@airJ{x@;8W!<|s|Chcqb@v{s zLv#m4JI^1Fnv7*EqHw^FJ}qQ<__fKPGR)HQ!To*ubK(pGF+81IWJ`I8v*X*~HzMuj znJ}=p(Cw)9Mp_B6emB_?UWvMujtLq-X|6A|YhhSxFuC)KEkRa-f40!qCZPf8XH|I14soSlVfhFnopi-)7PzCJ-|ST7@%`CNWm-wX zWvN&41R^CGwateL+c#dFor~&uh#NAH#|LTdw2-PsFvM>O6l=PB85}3>JrsNs>M`7f+x3Nq ztGd~c=~t~nIuyL$^XI@vu3^uu?8&QMkq@x~+i$*GCVyA>JS$&UGBT$)q| zWsu@aHj<0q$@sRXsqWdQSOh(y{IZPqQuPRQ>Sb!OGymH7XaE8mt+exAcy6Ec)G3wBRs%61A@$mux{*ig#E605phEGaZVnId?l=~gms zgV1DMPc3-(ApjUbjmZv?t~2lED~lcV_&TDqYxzRBFD2ii>>VU|L?`h~5sh0KTQi_{@{%g1_6nZ^)^5F`W5&=>LfrlruVWG- zDJz1zaDNKJtxeD92|F&~v z@D%I#`e;;c@*@U)O2SL_>?6upS+E_S9rk~ml<=9-*63~djv+c7q$976n{1`NE>At_ zTut$LGI1-l7^ErZnpZpcYb8n2cteQ<6YFMjFOKmBcQ@KZ*$H>UcU$II>TOhE)>|89 zovC)>dd&|rIe4?~78A)`gR?#l(xH3{)z#MMfSX~wGrpj9+Uqpl>6Lxx_UxcrI0Q4k z_T*_@dFVo|lm)ON|28ERPtWr#9Mhrp;_j=L@xW{qkLlyIcY6YgiR3^&2bvT%Ews5n zS8YmkQnad$3ZKGv-Zm|0uMjkRkqr0py6y2rbG_5yM9me^rmP!@jWgsz zBPeoZiHxPtQ)-rN*Ei)`qmP!0Qxdx=d|(2CV^gjB6x>a#aT}}ho~oX4&e#dH`<4J9 zs?GiKH1lK7Rzn@)P>EZqn@h_=CYa_bq>w6K3>1epJc=|%9}cV_eQW}| zaA>JD`1zolpLA-`E^oyUw>u?a7Sk=blY^+I%)X-6{_9z=WNz1+zwmdJgJf5;r zlcAfimz#oMQ$3xxO*pz?Q=hliKs1SYEaux2W(oJnX~?-N96DDy9(!?etIQzat0w3~ zn9>9PnUZd+GG@Q~W>vYFq8W2(0bQJ`1>5#sV-;QWG8|59orQCCTU7>nTlJs+j-ACvif!V5ip1D|M z^Y!`-^tS(B6Xhg6=j3M5a5BQc%heCS(k(=O9!r<<%O13-4kf_QJIAG9Ew`vStWF97oN=Wn!O}!`-q4 z9k8_BMnp_c75MqLgaj4(CXSwlGyP`e>)o7&<@73)8A4qk%@%wWsigA**I|p)z8$aV+)e zm2}@`=fB{_8G=#DF<>(|x<{ZiRXu9|`kb3Rv`m9@IE@>2IzQNeC0?%Xz8mxvJQ+ScnAs zcuEg!`GIi&RA(c94b^5^;{hkF5YF$!5qBki`Y)L1qdPY+(~5QahBYJEq7;Wb7OP8V zv*J7C6JjAY%vQu=+XdPc+oQLQ zOkx~xG>%fO0*cV+O6N&z!QEY1eCN8y1GPXJHm(?}j6%^sz=8NnB(qC-wi>*(*$?{U_^g_nNunM^izF z0bHE=B^-J3w$oSU-N8D-isEwzXp-fXUseS(hbls5EHRGq)Q(6t2&C= zGmzg4==73F3+KmAp-G~M3{(P&7PQngP%vi1ieofu`ZlO?jb270nzoNI3LgfKEx~<= zEh8b+p~eR@z3pP_)isPg6O?~b_ZdDr-}D=pZB9$OfYXFS98l!g($2OkyQO^!;XxBM zQ7WY?yd;zxFldb4&HU1d?huvP_$l*qWtByBzXr0!UM4T_GT=Hf%P*^KLEuEe$p0v&#t6#x?DoLuI?6c6xCILa(c}>-_s4 zWH7GS^ON7&aC%mG`i*qkayjcHOW1Ao_Y>TMsm<+k6R*0YaJwdS?Recg$pJ#}>-H*d zPp3(~jAAfb;6it$P*qNySSRUj53Ai3l6ZztzQ+fjk2N1$5sf`Wvuf4G^@}yc&>YpY z4h;3J{XKptIqM$f_ym;M>UisRjcsbg>L<~S^_V3rcabfMh(nQT8C&IyyivIw#Qj%q> zAefQLA$#VIRE9!LZLHoys6FmkK|M+ue?K&Ga0af3r=*x{b~tMWL&s~Y>PI7SGax(X zjC_9MyK`WfyWpJBBmQT{I+R{PY}y!ju(j8n@~Io8#A0>sGM_`Qb?DE+FAer+{rmkk z&cMHC?=(v7)qao*^KRo^bm!kLd-S&A-Z@KNAHgTG_>lnjqEHj%YJuH%e0iO1O>cNP zDT3vYybgFl0QFPtLcdEPM6|-4cLKP-20sgV;Zpb8CIu%BygP??AV%|qQ_!eEnXzqH zYS+0$;|zh~AN3of(@K7G8H*CpYp3!OS#gf7*C>AJ+Xw$|%6LO;Cw$>%tkP}7V8;(+ z0``5sme)rldZcPQcw|R)x(??dw#Fu3y*H^nr;_CLv zqnppXhKkuOOHz_hRf3wsVu7mm&nhD|>b$I(2lMm{o zpLKWF+$FIftDw*1JGNZbIHx6(2zrPcYBkB3W`rLPoBQ2avczHq7wRb9%3X3`UqaNa zSS}hGp!+pHoWPgg;5V5IVW%m<@@C*aMV2lpDqeYcL%}da=|@U%F{&(1gItA_#W!@G z?x`8~D&{r?f{G?@3uYANub?H+kR{dHXh`0PIZ5J!{kvBC_b$ugy|dL>G|~cVJ&XqJ zaJRET@UN@T3_c6*GnZqrYM*${9c-DHG~idN6Z%beSK>yJy?WUlPETtg@}4bcsR=2a zg`4%5{byT-}`cQ+vImNE7r8O5!$r93G^7!Q2QFtui;&!LWxUJ6m{nr4qHY7W0P zRQlf(3yco4!z>L|$+38A^rPVQShy|wX2s^~2ao6LtMJH3?W<^Y`=?%ea)^D<~IB6G@f3JOOJyopuE;&g0r_=%J+jbI7+E4(yOq2 zXDg_E)&6Jcj@9u-yTw^q+KN3|_LAb=UuR&cE5cPH2b@ZyO1`C}6Z2+K`vMbR)whKv zT0vb%h(_F6TzW<=V6lG$l<9SKD6Ff(jq^ruoio_p`7EQZ+kKSng!lkmgf`=^-Kw`I zbexyj>R$)7VeSjtKRt~C5i4}f%IKR2{|H!Pw~n%%CeUeX@ICG@9M0stVEl)*Gk4{= zgqGG{j0zvLWOTc!A&YpZZ%#Cay7Hp{`wun1rjmv8Y-X7=N7b%H$Uu|ei9#`g2KU}U z8Z?Phh+EYaPHpE;;0M?__1GPqBt137906*S* zftoWgP_{nsCae}ONLn2nadP;k;6kK2$`PiBUkyE4_DP*pXQ#1(8viT7=^dZ^F_v12 zd^|l3zb|REj%^;ep|=nsp^P8#FEYWAWs~jD)qL#5&aTTh%k4l2S#`0xk4B`4j<=ue zJh1ErAt%{3D7_r+I7J?-<=Fb(IbuO*3J^}wJ#WNW%J>m=Eyz^UyuvkjHu9rqPGz8ecW%B}G4m5>Qcsotl-X&+oP^m%2qB$i%nTh*`!$yU zS=!8?9qk3p?BDPl2$(&zW$d}BO{%Bm?=GdZ?nOW4KQf3|m1o)gdxSf$Hn=O27zue0 zd4qH9ESyfTh9(8;l(`}K98wld?1WjYuFnNuV)L8viT-i0?y2ePSnHhmfF0+ACgmlJhnObi-bE`nQ2*KXq(Y2NZm?ABo9~Ri?Rt4S z38noy7C**GT8y4K=#5HwCy36_q!+4;RhwQ50R!|{+T3o(e)_SiIW$-q-^H+7+{lI zLKB-x8rtk@U{Lv`{ZcAK`80SF5KOmI{3nEH%MV$LB0v>DqD zR}stZ&!Z>s8!E?bGme2L9-CNtlZ4_>fT@@??}x?R0X zI<*d<(}%uYDM)S=uwxYBvS(+H zUg5MOy7D^Ej!*ddg~jatb)_Mxr2R#{9`Z%pExFiFr|^I~vUD8j8>h=z&o{9f2Uo_2 zhjM$C48wpism^v+6Km~E;|Y1ZJD`4mf!)s&_+k>YEA6fT0mm9hn6BS|iPFQZ)Wogn zMp}%z|2i;#%VDik>25MR_ef9!C3Y|^jJXzWPon*NB)l)misW7c_SkaYFMg|ix9Em{ zfJ-In1mK~RhMECsir}qZw>s)pY-TDWAte4Pp~orZh|_qr?7sHZYwO1+j!^_FA(}^i zd?uQH)H8$*k~l1OB0e&&9e22XdoPYyzXM^7?vp(e{Cs7h@{>|P5+!6tzI~7KWA=D! z$(7SPw!0yDLh@K?f44xD)MIQ+RY_dV%bbpq)YYBtoXc0B{u!^oFz=qzg}xA&s6QBK zH>A75p>+0}W*r=*VCPreQ-fvp0JK|;?r=K2C?0qXmRhLIY*h_RIRUEW=E!2D2osja zFKu(I?K1EF1A;p~%4$p)C(0_3rZE2+Yl)70W3_Jw73xxRWrL2JbF9V4KU<$g$wgnbwOx?** zwMQ_8ROBFmHuAjRB2DVZUxjNm(b^_x3FL;so=?0o{WYWds4K32>yQH!wruDVR6&Uy z%R|-`>RDzpx>yLbnTBy-=8?e^l2$%SXj(Pj+;8CU<(XjpS7=c#9x~JbSTA>McT)1A z_#cgN+UX*A9VWLV(*7ZmH5v1%X05WtD^?-hEuseyHe+|xOmv(kFS5OT-HtLDzu{Nh zrLohnP%*J;7ucam49#qi4&ho8l0Fz46#^Q5`v_K`S%%H`QvR4{&d@t8 z!&a4m52(Me!_&Z;3-&A;8ICFT`a;mCrC(fI!sFKwwC#~~EXZ1ybl*TE7N55jFM3OP zI|m5PpNZ9?{mvpWCPomtYsCvL(&4-g0bfV5a^k<}6IyixKRnE|OGv??taq0$pRM$* zdtbMBrsn_wdudDtim3hT>K;UyJQ*tPkTRVHxw01{kjpOHh_ZTj-9lr z9xT)&0I#+)_{&V7F3H54ITe7UMjK*ditab;ztGj9Wj^P+S;My9HL#sC+E#*OQ08!1 z7??OAcssF#9Ndar`1HHYJo9<8y{BVf#~Uq7hHcP|`oICR*z^!0WjqhD;L#y$D{_o# zFN;5>vsBP>Q&|qkyD=3cs#q8uv&Wof@Bcijjpq#wRglG>>i~o*oKud;-#gx}a^p;? z$O|98hPi=w5sc>E5stiF*~nUJfhN67Zn>HA#%gQk?j?e3gw_?=ftK_8GBrf4};CaUDN=3wWP>WggOjvZgzpIB4By z4HM0UGu?{TX9=j0S7pS7Af{3vprWnIY8CZ|zhgG?e%Z?U1$pagkdhk}RkwScU&BT> zeb6)Aep-zCZ|bX&M)91)4&|*mT~{cQwzOm1fvbWwmQm&)4QFsS(pbjI?bKjUF83k< zhlOV|>ZU>cmc2vUA=3E$f8Bj*w07>rc)5FXy251nS69%|o?k>=RmuHU7)YhD7=PO@^lzQg8YbwMTV zr?1-#Z*6D)LSk2QMHbJywxOm)JB;C{4LLova_S_Dw+21D9@UN`BTwOHWKcg)PA&n^ zLDfNza}cGypKbQHzd82Rd#ucMUFZ_}gLKA8KOeBRY4}+&QtBnX$_^ejb;<1+fy7vA zsAO9%Nn}ARKWDfeo-lb8-YAj#CZyK26t-+yGICAiqjcNS#YwY~8Do4ZqV0xQoDZeGy~EY^W*y$wWKI!=q!a=xP*nq=&zUPTIzmdFsd zhpYfwU@wPI!6(s27tZt;S!e|=?%YO>j4=K+Mb_l@cvqB$5I& z&@v3n=VJd>?)9EBv+J9JT~&yTIWk`4F^e=QBJ{6TmP53O1q1wW~jg`|oi2 z$G?MdhosWrDSXQgGB68Rpi)~ankK=m9Q7h81C$b{rcH!BEBdk#oR=}dAf?OUF9#TH zjh8W3q@CY!+Vfc$c?>iSZw46c^E-TQ3ww0o3pdohrs|VQ;XrWcHu;w&jdYGdBWQyv zdYc=&y~23Gcsp)SskhXwqOO9SF*Vk%TC=m<&`XQ*FKPRWga7b-4t*~m;M_cD z{Zr% z(HZ*0?2Tz-S^cF8jq!pGMLUa%CG4s|YSbH?jlCs=a?RE=+_-(|c%kaf z$E);v&L-LY2W*|DJsj>Ys`yBCTiDD?0&*NfC&{PVht<&ci!n3sCyN5lS{m&o+{}HHk|HK^2es3?UvTTIE5t*yC*L7m?}4b zAxL_z%>Fm=ITrA495^rw*y!wppZ`+4)I=&tG>?R4a2yjG0u&y)I{saDmpY?Tz^)Hm zOHx+v$86yV>K@<=om7O?xG}M&H5|o(7%B#Sx+lJ97ONP9#JhPR@VQa#FA+~Na zqwPp5D&$H#ubTd@@d$4xt zs7#9|FUNxMg>;6(1@_Xp|78OX5HDZi9mXJ6Z+Z_x|z}5VI;6FV^^Pc)o_+7%K>s9-)T9zPC0hNoU!r^gDN9# zW?&%{pL9$ey)U3ilhI;{-N&VEy;fe>%!ni*8Lsp^z|j6x_I^PggT3~_3O?P^_1n?)Xsd|-LJ1I>D5c7$6=Xvw>2r1TTzC^jdoMAr21`@i zQK(hCc)J+{IqLnMjXSixZhFqV@kshMiF47LzNrFR&W@UU+rCQq&C7Uie=b>VBS2!W zC!BS4YjNoZyFvRgN5b^%dF0mfho3i4CVR64zvCOOZ0RY8z3t1ak)QvO>byFJL&og{ zCIq^W=jZl!npAO>WuztyeHNGTYBM9_K9)M9ERmpKjdZ8p=@j5eARh93{V}ki41O7& zKbuaO4Th3$sD|?7GZp}~Mj*KFK{h0Kb~I>{GA?`@nQ)K;Lgpt~N8^W=Ev?SFK>ewc z1$pVa%X4wpVW+$20S9irHC}5zk{RYR;_d_LF`grjH0Q-xwKsk|9}FoMAG;LLr8&;~ zID$Hi2&E8fNJGPw5lbXS)6Pu9nQ8@wJ}MP)691Prc)Mutmt~xhzapyZ>9cHFi>~vD zKz!)CoyFZ=LtLiF6;ofEo~iR-D)K455Q-RQFRBTJSCCSOA!vp*yQkIJ?-A}^4wgbK zHpjJ$xfCMhjcpY1-3TgsHMX215!b4Vdlz8%)5I% z&He2f4n`d~uzS+@to>_t{8KIM#cUMgvtLTmm-#FrxJK}5nV9V^n4xEqc=e(o+gsF1ao zRTllVj}pJl4UO#ozPM=-*W}0`>|>@hBLMloZ8(^?QO#IIeW@Wdk`ouc%nSYZTgNMa z4$IBMq-XQijoSpo5ukmlUlZNSwr19WP&^)%l>%9!-Q}4k$H?E-{?FUtQsy6m;{YLB zu_jW>sc6e(m|5-a5!=I0^wS}yx{h9+HMyMb#xh`o(!POiO*wjIL#yV=nBooD1hqq0 zm=N=jlS|eKvS!)(dY(|XO$B{@0vo+pER(yOYn>WXC1b~IbZnq_v&4*S!|r(_=)tQ| z)M_JKp0aWh|HPDQpjGDLU!>ywApWO&HDOz~zkqF4QRbnk0n4|WVQtu)GfE8WcT0m4 zx;sDMl&>eI)2*o+2q_fLE$T+TXH<{HJ0fj~$kqNtWV)JDVB*#t;x8mcvTfdDs}zna zdGymmg{L&`Sf-L-k0?{)lMp>}J*;)~Ii7!kb-8{$tT_|Ub0+FOlSoNeUV2btAa+)Y zTeqAiVAE%Ma0Iy-7ZCA9Zil*q2=!Sa8_!;SXU#J7+_H!~7C_v%i23$fm8QX7O^MAb zBGwMt%62CutNVBaVfJSLos}HPJ(g!=26m})dq6pe3*6D(w7qeyK!N>{(2Qm6v#xE@ zS&UZG;(G*g%pO;&3oY3lt2UHqV8G*d=LRjUBYWO?>~~g?o*{G`S5{b?wwqXI1;sln z7QUxo)Ymdg1x~Ker_FBL)~qj3 z@&+#S#{=4A2C5&5*wRhZ&C}%=dcbVevlrHz6(h#2rA314Y({?>+|Xo@N%VHRow0xC zqKy=K!y@5hYZW!(yTS|UaLv71XM*;90A+BjWxdY^L#lo-STaEqY*7nlQ7AL6f_2!v*ndB%>3%>5^8G zcMBaEZ2>JA+W4W2(FQETkXYPvH%Y)EUf5iC^~C*xq#CZ4ocge|+&NBnVbjQB?5T3N zG~Yw!-Lt?+{Ok4(YAG0g!+#O^bFS}VS%g27O#Sz%0;D>X6OSnUmNWt;b?dLMLFE*k zHKTF8!Z4J2go67{%p({=IRR2+Acf~V-uQKQGv;Q{#fAGvOU)D<5%z#Rk{7!310<}y zwR?9$wQqQh{A|@fsyq)Qtqdw8O(5qgAv+InSG>opU6^YJkSy6v;T1siB(OCpPe}12 z&Sm?mLFjYZUG(qDOYWA8o#O)4iQ;Bfpy zy*%+TMeq=x!^3&|-)fY|GUgMP4$b#`K#y%7lBp9$%MKk`{(d&>hj!NYR%LHY5AfZM9TYo#EQq-n*W&zK zOwbqUP{~%%8m9M71=gte2EJ4?9~ri|`KEexBSOynjxzq_=abdu_X>}_e8zR{vM{gw z*KaRH7WDr7`8IR(n#G~vSJ#NZjmar6&P#)3Su)U-hv4%8)1#iD#{&a;8Bvl7KQ7;e z4L{-d`xLf#@3f$FEXN?%v=Wo_J-w5h?L}cFHXToxcauhx&d@`c;~QYzis(>BcFOAS z{wBFE2+`8^Ts0(@*py6*RkKR@D--mlTk|z zMVQ8j>-SzNoe<8v|Ah5%vT5eonQHSYl}l9-Ps8hVFM)b59@1Wsxugfl|DXdEID}SV@QAP5Zj7+pXGXbm@f1(+pUM0A~6HLw1y^oLG(pytUj^1OARg^AkdPSnn8EYm*@5ex5 z4+&p<56IAG4#-n(Uc<_$895Yf?whesiAx>Bs~vSxHU1aSoKX3caMcfU<4CYQ^H^CH zWdqYAhi^t!K(g0`%k<8_q&4*xLn}1 zbsB%ge17$Vu+E`^`dv8K`&u3@b2aOm$~FE%D+%^6yy!2GGlDONJJ=YwNi!+IYS>F1 ztQ}q6#>udj02zuS8h0zV*T)IGN2su|`G z&6aloqd@&}8Po>@H3Ad(4^qO-z3!6S0S1YMymN2*^+*zmv1Qh$U-~e3ppdb3Rf3)pyqdv8@w+EMrO^6u zA4`}{rZg}ZwL4+d(=b8oa*YJ^acgYm#Y6jWx;wAK!aUTMCdn`7^h&SV^91V2Cm?h+ zc=opv!{4CIBf^{YUoh_~g~jFa30XlN5vEG#O8nnrVH{2P=B}qKnjzB;mDg18F_*1a zqr7k*ZbY0G>N)>5q4M>GbsVuKWj$WR) zHI4o?@yjaZEyVVklQBRjUlNZ+ki{0{d3KNVMBu+22I~>;X3#IInA$q@4hveG`uy?Y z9p4m@Fi6Jw^Xyn%iRP)<3JwQltBp87DgtbUB^wUUdx6tpYW2p`Vc=LCq@V9)uqpe` zxf};a!kGW@<4+~MNJ9nuf$D*~nVE=)+-J-y}9zPHHSWH8}4&3 zBbhFo1j3R}49M*Gc4p6LC2ix&U)OJCEXkn+{{NF?la$c41~%ULxfg-Ei)g(ab0`Qvd)>W2!`f>28HCn!ohe4- z1doE)_2ZvXEk8W?;Cx(-`90!f(6fbcYA`x07CNTRkyUR|5*FrGWE-+Mu7KRz6ucP z{$&!bwtoHbaEu9<-00NX%}B-!S0qj2rw7ReTVE9e5$40gO#5@2r?{sMBZ1`HpPRBj zv;z(u4s0_nMpP)Aj6p0uJ<5CWxg~m3>&_GO=3%sGt=F~reKyiu%(ns#6k-KRDjrn` zobK3o@X{Hc{XjA8)t=FN^w3e$I>e2Lr=#!8iAnBMR^bot83jk<)}t84B&IkY^%DM) zuQS|!feSxY_VWX&j0VsG1hkWE)!&?YZR!cLX2guX94m;CXSG6&)kY z8IDGt2DVEBwkbE$?%^K@CbyfBUxPssop9GEbg=y~R6B_{gtN5;DZ#wrGw`*@Xh03K zQ}8M-@{+}ShR*KfT(t``?>-K`PjqL@^T4V#Ge@-klq$_|C9^CFtLTW$6_^DbF77iy zS6B=Kq2)9h#=OPYj=NtRJ69mr|3uTFVk^Uy-dp>-IdWY3^PRRM7@c?DaG0joJ0wFW z$E4vlkp6!)G%vM~#5Zzr`^*4GACK7-`C388x+CL)x5CHl@}J+;ybf7qgmOS>{-a%* z@9r@_?#X#zH&2vVe@r(ZNr>@f<>GyOW#Hx$^ftSFe|}P?@@=(8pKt(KIyC+kj>S`* z$_{KSpxjiR8skB#{e%{kKayy}VV&izfc@%DkEg~tlt`PYA7o;9nHTX3@XhS>NTN>t zk^1TKLl!Cg;zp#Z<>Qix*eePoEj?UNG-pY!BN#@8YMm6INgmsW>F^ArnLfpJ_<0#X z%3c0fI>Lw@dy>BDjvka)6P92HDng+q>Oei2>BhO>s!wReuTnkFa^F9M0wV_^Zh7q7 zKE>^pi%onHhDd{Gn~7s*=8h^yFRXO84QsnCttey^qIw&W4NS?AygO;tfYX*tnDZ&R zer_Vf(2t5!ZoJTSUn2xev5yTq+05@UhCJi|BKrn4`z;p0jBUw+^yd0?&D#vOpD-t5uLks zaVv~6Z^5gGdBK>%X?O|pw68o=G_Nv$hUEb4b<(0*v4U^P?tQ-MQJ2<#!kx}0hHZ*E z(+?PhGsjK>AL_;pUdddk^dQt{m>}SlWB9TOPPPS}sAZmrXPLkDgycCKNGC~$5h0(&qF+wgNXPz3M z6;*sKz6fY;o~WYGMRYc^gMGONGhf}hO!n*?b8@~*&i+q__STNj(16W25EC0T_BF}6 z4KBVEE^E<}_VWx?f%_iNg*ui;r&Xj0Q_oblis-(?g_qgoHA+LAY(v&8M4IHDhOxO5WEG8ufic@*A0_Y#`X7Iw&lsEXYF||1*Wtn6 zo438{DKk5&QB^Xwje?dy*tXgf=Z!nA0x6q*dL<#%2+W{xMSayK-&`sAKY;cAm0a_SX&i@AC|wZZjrTkeb=j z&YG177ie+Xst|m=i>{vQ36b-oLTt_-l3w}5K}(Yx&0epzZvMXhv@hk8hjo3!o>yb# z{PI9%*;59pUOHGHVoJX;_x<3>sNeaG4ASC94ES+0`%Wk5u;g;lOsW7G&Nr1+- z8ebS$P{0>C7viWLV+;<$1?r{02SKjluzpI%X_}Kl4Tr2&QDH4k#EtBBX1Gb(tT|JH z! z8yp6NdQ?B8yi^MmGr0L&c9oU3`wZ2vJW1s?63{YrMF>Bpni>L)q6 ze?J(#1so?aFDn~oCyyx!7&Q27137`vUyG_hvlR5%k4);{-5a`!sHim?f>379UKXgn zSAbiW4(jdIyG`Jx;Mpv>IAzjo*<|rbe3rWl1*X2Mb(~i7iZrhYlZX*Ow(3(MRA|io zY(@zlRs94pEhMDw)W;Uol!%Nadtp@)LXnjw;r-~YRs1!@{+r`0 zDdtMG=rT>+4RHM}&jM6Jh&|+(`EVHT>^S_IW@IwV*>8W1@*@RO_8C`EPj~z|gQU%jC>!f7j7Eb6uIAsy`S&u(O8A_YSV;5KI2JlPOL<>4c=siM{+iC6Z|lY2!HahsMu2`USCbm*x69HFYbt?5`s# z$OkmRW3>jZy$>TMKA+88=*U0e<)C!D&G=TM8J|6{Xvst*+Q)LvSZ*DsAWMsWT)jS0 zQO9<7YyJ{_mDA+GtrS;|SeqSadgsx|PzZ z@*Fx4ZB`*rODLo0ZUoaV^}b*@&}@tW@|?^T;ZY&B0QclCzTXW04`sjtnsUgjH}>rq z3NbI^vhfb5KuVBElIsC_rD<;!BLouLreb~+CP+H|zeGu5xaWQGAJQnpl^m#0pI0f;!RPl zbJCZ>u|hPuF_o-A$nm7h9h*AX32i;22>kR~?vfsL=w!tD16yzwd|#3%Oq#$H2#u@f z5`WYi4<57sVNTU^OOD53o(^Xs*c&Y!f&KcSti|2d4=NPCvuNSi-=&1n)}%3R1z|v* z1g?Mf()^6KS?8Sv3%dSQOgf87L0bE|x*M=R3=^`ZpA^xF+Z#toOity~Pv8Nw(nBmq zP{#LV6F!sboR(Fghb-!biNDzt=*W*dYJhurfg%_5vRc&A1Cirjd!q1C9TD(C6V_!g z0=3<4-R`OTzJvW4+|+UR{?Z+a{X1Fdt`6S=iy(Y|aLSacR(2xBJgs~@DxR#Teu9rQ zo1&vc_7o{_mH^Sc`^Vu~kpbGAd%%;Vvpx~Xu)wWW9&le0NbC;D!3ym~kI_xw)jPPb z6WV|)@?iSs35|H1XRXm(Lg+mT{IO8o0h}}1{l7eDM%?a7BY&aHsdAqSpF#-spy9 zyX=}ltrP(nI>iTM=sJlvewV%?Qd$@s<*-)%XsGXi6j>;A`0;N44K^O`AtokQowP5K zt%eq2uZA6XyE9@N$Lu((O3L@7h=RwU$YAZ*tbpV)y`ClBmQ)HSVeV%H=m(E*RvpsM zdW?5=@O-7A=qDXIE(bOqpxCK>|HhXet^enMfwf^GqO`7i3D{%(G2iZb^`<1_z4RK1 zxAzId`>iR7hoqXf!WvY{SD?b#uV7dDzhIu~e=r(JT(orX(yX!uVzJL$CVsxmITx{b zkMQL|?vBzYamuy&04ALISRPQRA>X0%BDZwyIHh2xuBy8j9rliOm?d3Eis9rDQVOfrA;4Li3d_m6>5X477;Orziu5uH4{ICVyCc@m1f;yJ-w-ux8WzfpU81I-eT zPXI2`$MF&Dl~&>XU0wBehJF+|JtkhtcJXpBt_@k8h7~SfM^%i*9KP-Vi<;fkuCe1C z<>ztVe;ZYiZHDB(zmiu_Sy*)Z^gaJ(zAaqPgF5CgGk%xD!ZlA5OE!5kf2)F$ksUR3 zrXCn}(h^`TKs^W+JRo>I!GjD68b6c>dX|?a+0OB7JeK#T>DX7$^&&o6udKjDlkKhc z@JxI(H&-n*C`q3WP(S~9?OguTs|DcB-I!QyFH^^!Q{~WlgF1X9B72%WGhI11e&G5z4$Tz3k?4Y!dw*hMd5AlFpq)?_w!}TWt zZD1PID4_7}T2JWfqmc3)J^InoNCUmwSvg2<6QZJ~Zan!4xta71-2_~%`R))NQU*?d zKY`zv?njyGUR(snTII?@@R1p+~v%a+RkjgQx9(%eh*)2_}PtOxsDwhY4(Z*dneetP3_K zL+`!37T>xL?hrTNPPm`~OS_`Y$6f#_N}%e?rdR$`E(|XZ#|+e`H4C!#m^r`h@)zw4 zOPMrNek0L8U*&-$k}2X4PaU-M+;u?nW%G_H$u}KLMAoc_5x^RdxvPppKXz4q|Lupo za=^dF19{GP1gu!@+xVWyi;kB>H{IA;lZ4HxP8pf`T=V0*C*3zGYBV-T;oe0fA{BaR zlqd~qC6dv00ABI^#2QAc&Ba2A?_-QKNTj=*_$9Apck(ekuS$8j!1&VflJ#_EK|Qw( zk9A&!IM%#fgbI)iws2VH2;k?hCw+naZH4OV)B&sWJyQSOl8AV{^?UlHnt^RBi^>!y z&%Qs{nJiw1l!YAfaX%5@dEc)Eq`fH%&n0%bd(~%n>UV?GfHK|42Fl6EWSIkH9q9UR zyt`h;5pU}u`6c|nP_`hd8Rn&9>S-Qu?*Zfs_tVXN(L_TwQfB6UiP-sS*w8IvXO#5K zV|nToK)p3EgNnAOO*W7qyYK>~skq?em0R$-{md`=xsUK+$djc~x6n_~FOcQTgqM<8 zwS-ylFjGu$^KxMR-KO5we|{<+9H1`cY=J9Rij_Az_%CY{pVi+Q=X74{vP6SH=_c<~ zaOt0xZGo!=O$#V64k@Mh@vZKU|EF!;LO_5Zr>PrF&L=mKz~O+L;@J-1+$C<*O9Bs7 zuplo@Py77tVA312^{=>?$H)XqJdUEpf_j(BPzE9$cc7!%HXbxTe*Ko{pdl5~u2_cA zVoj%TY#V~-hO}>v7u&%F-GNV&Xd7!Kurg}TvpZ;W_N)q<8g5qTm;pT|kXENqlP;&l zG!kHZXe}Ds8~@VrOs?6F9oCUr$J8G9Bc>O!U#ovpwcIjA!7S9Opni82Fup&MOc*4* z2UmkB5}`nxc*#GQ)-`OKaxbj`-zsQ&%qH!%dcckKBV8eM%d4nA^(`E(oAzbKc9d!b z4^Et$KD77n^PSkSi{K%CXW#MZ+KaSjJZlcQo-PzcOaW}iEabn#|z&l`Q zsphRvZL_}RFTn-mtR&(&36I;4$k4qt|)-{T(}qWUiU4nFlj+*#z6CsC(scUPl2n0HW%NEEuhP7waR9z)>6;n*EO#gyTZ zDE|83aX0W<6|V~KS800{tsXwH|5jV#Uj*wKb`S*gdG@E8o4UXQ{iO-Dj4Tw0ak3DZ z${TFySt8ic)D@9V3sQP{0X7JxsUGhbv*Ec9l(9vg^}rBWZfN($du8eNuRbC%+wEj3Z4?kX zATfH@7R_F{-=g2S+w|_x?ki?YB5$6HSE2E*&wydL8(_}@M-{}=#|)JUc&%s&UZMhl z){b=$jL%esBCjPOFRDPzvmVJJNx)U8X&-h#_ZFaY;91@@EV(X8&~rSTwnVZA+r0q6 z$lctTz*ROGsXzCHw<9&^uF+Ncv0k-q0Ev_t`F6SKE2Ey_1L7J{x%+m(5eWq-L>? zGaz-I2|Q`8>8E_W>iG=pspJ~V_I^`dkYWk-KxaH_1M@ctSjKivFcHD7_izuchI|D4 z%Z4wZKt-L_+jqq{f;g}zX;pXmAO(xhN`kw<3vN2$6;un@g*<%Y!WXCS-N*8ya=Gq8 znTB+~lkFe@1E>2-lbOQOVc!HSs&wM)X3FRe|(T$qT`KGh3QQ#kYF??iOi**&$WI0@r+;1u>mq?Zf?mVsNW z%Q}P6oLlFAn)c4UJN65}_|X#c-MaG4_iz5_2;3&05L&0(y63q$!P7nYILYX(=MT;D zFbl%s?kwajV}zqRX{Z5$laKnrsgva|Pimny#^4ng|n9)uB zBx31QdYwtUl<(qIYfN0+2~%UxcC<;nIdv)2!r?bZfCztmu_Pvf5!1LU+|VE-PXAwG@!~E|ZS$>C529nk(4N5S|4+TPQZ7HFnWOrNh}inCW0~ zYao*&g}ALcjI&w&7Zu~GSSt>WsKHSBgSbcT8khdwv6yB5m7iJt7wy|H=Pg1zKX7Yk zo)9UPq*{zB|~1mS9CLbkQgQ5TR_F9y>hb zTL>*l!pP59JRZaHf=08{f7#c2t%kM>M6SE-XSThIhX_Tq?BjC$O1$bjk!u4&jY4E8 z3$3^Y7JXTs(+V|q@>1J- zPNt31jC-(?Y5jLHEf8C0St31j!l`xWQfdZkqu2CY5gk=@TF2RwWnc5~vha2Vz1!he z-}=z*h(=%dPjPQSSD#QA8;A)rrQ%~LwTUR~ZE>ds72N1hdIhX?Wi)Lqrj8k3nhFcgEdom& zh74%dNV()EH}X;Qny-K}G|SE+pYVskE}E2K!cG5zAvn>#v-tK9H+Jk-xWW8BX|V%UxOjg|h}?g{c05P1u5?BD zkJQm{w1XiE$rMA=h+9xPE9LwK>XDgXaq#a+lUNTO=~^r%v09w$%u90HieY@IaYFEP z#T~NpVm{hZu#OTyxQNC^Uh0st_o^ zyQ8D_ZiJM(ZI3^^|CFS6e$+MRqczc+Tkcny38*@gLTJ~~-5A%Tc73xIsq&MAr87(S z=)5E*Hy@9nf0ER3_Oo(XUA^z9z@e&j<`itg8nmG>)yqE_ATA;nju;$?X{}o>{D*x; z8(ew&p#!f|yRzemfBP|Sx3Ws4ZxX7`lE?Q*QQ|q@kRD(1E0sn{*RA1uh-5sL!&z{D%K0y#F@+ZhFlX{<-fln|*VdlmNF>FS7&c@3Uq zHkRD-TBInlJWQx)(N8hY^4tVFJo(0JF4%#!#X=<1BIXH?+{YbO`Lj__R+pGAt9X}J zUCuLSkOJxwUDnunv>Z;EUxNnlv2Ole-1 zKxdqKf^ifS_sY!?3*=1IGZO^y{F-o+p=6x z8kd=f70I)ZETV4CM;b}d!IJli6c|f@=;iv+Ka=*HR_Y#bHt9fxf`ZUX=Jk`W_e!lb zd(A@p_t=~t_<)8ufX)BbEwcu@RbY!S38iUGQ|>K;qUZOqgPmUE@Jn}o z#-{r=UzTx!gGOs!bRu!8q~|w;;Sv6ESOw69Ya&lB92)HR*fv|Z!Ht&S+JerqQqFEr zo*lrvJQ+#9WG@{-8%f-2tCMu4y~cr7@DDr7!%lmwEk8!?pv8CP9`wB-K6-Ze}3C z-vpCoPL&Oy>o;BHPe74-l;Gm8`^7OAw$lCHViS8s;UD$1Z)oJ>oiT9p-{irIy_al& zBOCth(rS~`Z@~rjGrIJLAK>3e&)3(_75K^d1;qRq^SW(PGijI?tsQV=_2ox>ezn#! zoi1AsEm(KZkGYe)-EHi?_x#&$f3CLv$;i2__p`wvb5nFN%0ehyc>C8JQ`RC(lUjzYy0Dd{5T@x@7=+EH@HF#8Ot!Q&6-d@$ zd${>&PzQc`_Ud8IC5;0tNI7n_P7lKl7Z+4TM--~UXtZ-B5-Rx!CQk3Co;w&MEg-ti z3v9c8Iu1WqgV92PI3YD^7J_GEK6twH#~b|X_lT=b2oJli9KPvKSEkXV$E&-k^zo|c z;DTpStRnM<<(i{8+4+dTTZdgAd)lAhcWOP!R(x7(j})+C>8dJpoDg@_Ui~6 zQ*PI=oZ4CTr$yxvoOq02&}EU77O4VG5uIf8I#1S>0!PsTc^#7<>_KD4;jZk81NvK6 z;MFYtNGlUL!oHFqf7ZV!&hgO7Z=wsb6a`QMnQ_;l2!=7nb&_rUd3$WviFRxKsWWU-=aYv(?dufqJhz;F-n|=gS7m*} z%8bUb@zNHCAaxd6fCBBeWq-u95g4n#MMrFYT6zRuX<4<%AyP zKjv_gkJ?+E+?ZM_OND0N^)!l>*7WP881)MnVCW~gnOUB&}AH<*dR+RKIIiJ-H{*ubDFR$ z$ALCCl0QwJy!Yj&j(Cnj3b{m+ zf#4x@l6XdaQuN2s1vM+hsB?suvV?C()-w@%mJwx1RbliroX{aUD?uI8=cdPGund`t zzqXB^e^kp{f#TU}9`h|k(&of-#{?Kp}b(pq|XGG;J})Y1@ykXJLt-D>kur1Ozcnk9%t) zz$VB2QUqxO%GS90YuYyR>0PkvW;&6UBqwH6 zP2`nKv$6d@iCNpy6z=aBURlUTPN`If;j~AU zmi88*f69w?ECeO~aE0j3qZO*fbUMV`oMjJ<5|JQZK$FKbKKAUv|H2*OsX5$*>XyGO zN1#C|bzQH{DEyN`b0omd+*A8a!XmBD(n{y>7#!IFkF?ieEAm2pLsyE2X7zc{S(7x= z@?%1DYiwMetH9Z)Guylr%Gn@hH#1dz6#3z8Z^9|zHr*_&k?6AVqAAluAZ^R{-In`9Z^gIY*2a}y{^{pahO@WiSjoJH ztVY;t3eJBn^pCpV!w!$}53fUbo`+L)sI%<-_ERn4Aa@&(^L5;|R|W}e96eEWY(eak zTkIdiD%y~ENi~YS6vg|#)J4_{tT>_sY9I!WtHNXzRoOXBWFv%>cAgS!qgPg)fbAClc?0-AWZRM z8$I9m1ai(8QLz#;0%mkpDmVKxcI$K_p{${DUdnxINcl9T<-&7|um)N>$1>|hkg!RA zKBTTTa&Xf|1FZf6n{~P^?o<3dibmAWX`C& zhh2q5e~%%M363Obkh$F(5S?B84sp|0Sv}~p26yO(N|NU`r{&A9z=Z(O9U`F`T>pLg zs-(3Xch`uF>5F*ulEsV!#SiKAsMBVRu)#6Kat`Unjf8Ufj$11@q(lCmyJ*EKBCfwo zr`V5ccICyVmvSu7_!m;nAx$Wf=4^N=aZ===pbBA@sJLpptL8@r_a*B`2!U=!nGWji zEtB?xXs1||Jg;~`0vqpCf*6J)_r9b!mh&S+_8n_~b96Gl9IVJ0;8olo!V1}HUPka} zEPCO?s5DNgdKoJa&Wqf*U-n3AffyEw7^WkNb?~gW(KLpQj|Q^w>V4vWP-2#^lX}uO zrCYxM`mi;-eD>-?sJ~OKTo>ImH`SA3cjlS)z)fAM5mzpTl;URUfEgVPoMt84s<5fA zyxbkog6Ka%?AAvXRC=QNN7|JK3KZK;x2JA@^?>fjbdCn5Axsoja8Nq4TkKE3lJ;%H zgYEG)2>_L`7vlVrxKNpwH$I}o3h+tCZletC4VGs$IlVSJDhF9s)!9S)TIdXVEG8ib zk#Lm11lRRH0O%A`G>A@Rty-yJ?esJ+oRA^CkCifYjsf?_rw;gR%Ic_jWu(dDDy@>z*x$b4>NN@lEG-m;j!fGmGSTOd zySxyAPvYvOc>(4d`G^9a+bo7|_+`F5oL)*M*giz)Ze-}Q7WxOe2@A_51PhISjFk96 zTyhbWml47I7jr!`NlxX(AZj_%X|!xgn08_rKHlAXHEl`e@*^JCfpFq!K7xno&GzY}E>cjFZon3|b;96HI_CO8$122# z&_ZHog5g~koXZ>JzaMCHNPgke!OQzs@;k%R-dar{Z#s+4o36za7Awrct~%LP~T zg7g7j#_2)cdO2Kj3D=TqHJl7M8-DgXRRpGdj=dsD9?GZGF1n~naS#l&v+*t=k zw$d z^^I2oLbs#prOMwSt)*!3Mh)sUXiUf@bQS;S0^zhOhErzD+yM) zgirjA_xifnEc*}951-D`HG~|a(0!Dn^f*)cU7rp?Vu52eawHXXctw-NPzUljB2ClQ z3r)aN^6Mz`{p=)OogkVkD~;sao*t?I5${{mkjdDm4jPTsMh~5Q!f0*%

        ^X71C*< zY)L7G)fh2iP2VT`_9j~;Sv>ILG;8gL8%Q3qkwqL}!iXzvB77Q-f`(Qd2=-z-Q^89|uTK1P11Vrc_lS~w#J3zM-fVJ* zP<z3pXTcpBj5a+e*-FKJhwe{CF0^ zgqTL8oorKt0@!P-2H2{`Z6_dnZi=z3?mu{YXa0ggDYp(*m;mu+ag*|Qkx$Y1gEiY% z2IEHqRgnqn1-O}EpMbmZeM(R*^f82A;*zjLmv>htf}@XpC=*AxY+HjXli=g2Y1VFv*BHoqYqD=)+4ZU%?d6Eqe-<<#>+7Ab9L#;RTqG zAj(@DEAf1^@05v7?{|s2D8fIfQZPCxIi8Q@Rgc781?n|dN`p1jGC9UpF#72H(VDv~ z>j8$#>7><+W3EY)XKVKI);#wL78cuxlNgU;9r?rbU}K8!zB}0&L4^ylo3eYn)$+Hv zbs8_4>elR$GNV8EcG!Fkki47K`p=c&a!n;>kd0dc;sKq9E{Q}FjXpYJDmnV4GF;C6 zuB)A_hVY)6g4rh}zolqa)2mA`mNP7B?Iumybd(RaOB&*p<*BArnRlbFsUk|2@#x0{ zu62#n^e>1Zbq&P$ASAj+pRCF2g>IkXhH3pV!r!QDQa*QJ4Q?*p>plU&vE^@OK-aNy z5Iqw2f0)%~#;jEy)cwJ4K^Ok1NyBNS<;RDxl0P`QBwT^zTArBZgXA7nYVd*^)LPJL z8#klNAM-F=LPV9RcXANlJT}@B=Um|1PvtR-C+LH7sL7b5WN0RyBJWJGc+O6e z&`F{tvC=HTagJ2L5f7`@q;;Xp6b0SKmEk7&vf?|d=qpZ9Lz*Tg({zG|W!FY%n@QIGsqex;@ zZ0R@h3*C`}M^;__1}3zwClV{R@reg@`X7M~f$Y9L2&%2S0~ zmN;t-q4MaX^bk{e;Y>mU_H30uz2g^s^D=EJH3WX+?i}Dpx6ff;h~r)<8Sa&=izfCLmplimA`!n9xc&^M5GfhR|GDsYxfWNy$t>Fh+Wv z$3rznDO{8AK*|lBFYG|O-a&$4QAM&Eb|-}8=-)bw0@qh_+m;179xR<=We>O`GnSBO zD5(Po@>7eh*I$M&W;T91%4v2(2NHP;D4vPuWzD;Y&d_o~uHD{Pr%M>$`$@aE&{HvB zHeq!egIm5CFsjV1elU)PF`ZGS5VZSViXjUPp^)e>ik1|`CZw~22dV0ehQ}bvFKfi| z*>mv0BQmDZ^t%A56#j+!w+zo16IPZY1dA{K*l6N?Lc19%)yjZclv69qX??Za5}fPd zye2&^C%RODvWQOi`Z`bUm74qPWCuFPejx75k#a@*>B(qhMlg+sT~c+Igd-SDG1?Dw ze&9DzGxx%xkBbYpoZ_7i5{nCCHN7Yg5t*!F5(30moCF>tX?8NwvGV_rqm16`*_l3^ zFOIvbERPxZ9 zvfpbxAPs*=>X~s5AFJ!iua;kD1`Ia?HpC{xv|kTCwW6?TN8A*JE~4YMtNX9++kbDO zDSX2BEkg21dc7!bnN$s}{X%Qbx0c~!hnAA!N> zCcYAuCC5B4!iW*{mMZB#yVKy^MKoW536bjD`U(cv;G)|0_vXQz^G5#I7Fg{FVoy72 zW`~P;Nzt;6+SO~=Fe#VNI05TIV>fqu3M&UQYln;Nw1D4^0WKDIRAc?JMGRj$oeL!8 zcu$jKf~_Bv)K-7wZ%&5P$nw@Saw{>!8uj20uWl6w)ham(?ZjIt=uBJ2dgRhmw?jC6_x_DjzeYI-e z#+|?h0ruOmU3dywyRStIdE5}+Z#mtDL z$H;LxfyWysYEXVGEbN>Lq|*?o^ibadOm+OofG(%0gi;a0$}n1w%zKLI35|U`oggzm z35c2%XSHKipV>{wwyFBEr+iIvkj9%mp!{n5^pXjqMsR-A^p1Uy{!wHgc8HTQ71)H} zd8J}R_ZD&e8vvpbInaD5v2?K~X5tyI1^w#zjf3?wpX8&7)&A%UyTsR3@wcDe7!uYh zLNwpuY&D{7a;(ZHe!4IujTwp+d^7DSqd=>-93xFT28^v4Y6HpclRry;ZGs0<}El8c= z_4Se5D~&~;75w!TpB28#%0yuq?a;6>dypJF)nFv63x8K z1-Kq2#h^#k(2aW3>!U}S^UQwEzA2U51oogrzH9-Ht=OXr-?k4xc(Yy5EfOf_4fkZ= zxMGRjxSG;%X@9u^88|E^@zA~`40pZ^*^{;%G$RXl1$-m;TiS5v*8)!B&=K43r|@`- zTdHH@Q`k`n*wiqQVzRCkVI^wZg1jGPE>%G{VHz2SOs}%k-}nFuO1tC`^Nji>v<_JZljO^l4}Ez!J}sc|g0F_X@>sV?eFUu>z`-q9pJgUZ_~{ zP08)$C|IsEE|m)Y911XW;GQDze2{p@w+rlnEa^NVq?Y_(qCkZji#IBMUc%nS04b+a zj{Og5D>{;3#W*vF`!Cx`!_z%`ZwTDobycKVDe7;iv*)*1eA7D7_3iiU_5+cCWSW!g z2QA%I7NbX7+?Df2nY`+glMU}sP_k$?6c6LD(F#g7;S25DyGbjVN?|FgOqEc(>y`H>8#N>W+WH@ zI3~y~kC0Bw@jo@Ho7G`co~iwgHyRK;AnN@M|F?bAi5mj9a&BUDLTDda@eyj%QmW=2 z_#V!r5ksx0_mlzR-lVVGxZn0zFl|VdVkdt%e$a}ZZ(pS0s#emx@P&3{#pxHQ?*$H8 zITK%iSoH9J8KZyU0IZS*s9G5!Hh#Im@*RnumE7WDhm_v4do8*E!b zCnlSJd(5d8$*>C^c#!XL-euo;;OO*q;=$23k1r-_d4z9qt$w#)F{H@8co$d>EOORA zseOAmU0wIMGWxb%@xAStIq|4R0p`?u;V(re*4pFLfdfT$)a=9gtr4d-;hK|yW)Bwc z4cjDSMjjYM-+R0*&^nW2qyP2R+tHt8=l|IpDc<+brqQ0W?k&eFOIIqd$6AC(=>(5m zzoPm&@a)97Kfm8k?tA1nId^*e-YXmPj3|?VbBP&Wwil+uXBHFj(G8EnPUaxQ4Ug{i ze9@dA_Q-VK`J;(v5;>#qRED{$A~WXxDXhD(@FpwT#pUw;lGP~rO+$gO7TQEdp&tJQ zjZ+=ybXs(0^j1HOEI?A)=9xAs?5_eNB$HMHM59Ck>) z^_oc{XN7sDPUP<_&W0O?`-bcMxVCs7*k92T)sJf0KZ*UPQ#h~%853gG{pjuyyFX>G zuiNAwxP6eaEv6Na6qPseWjPy#!mjznpI>{pPh?-Pv-$7AHl4GR7w1mics=`6RXj@b z=wk{kv^;RqaHCs$BBH`2<3nfSC)ricOc7nppee+5>fqj=OUXRFX(cr;n4e8Atjx}j zRN8F~gsYAOp56X6Kk`jXH?qm^UTNeL{cmZp9-bQ*Xh}`|``T>2$5?dRC&`z0TfRTd zG~BRM`*!RP+k$O|9tD%P;d8>5lk-#M+Qz%-1Tzp~AY{IheePo~U_F-^os4 zP{|WUS&*#vGto;%X1>_4S?zKoNaP#*cy+^aoFh2bIc{sxy{uW%YUhWkYX1*kZygqO z^mUKU3^Q~$0s@kfqtY!QA|;}9j36PBf^-a^AT6MPG=d-v(mk|viF7F4-E|M&_x=6u z@4e5x_aAtkd35;9nRE8pd+oK>#(ZB#1y^zHY{rR31NiO07)Qx4tbsO+ku=-e=Ayzc ziPeL2nw|6{nOFhMD=N!RI6^!?@lY9!1#&W0j1x`$cbfJYJUrbVlL6f%Op zJVzCNhzc!oyBK+UQc5n&fH&e>ZOh}Q7^|rG!M6F{a9M`0#I*g=K2*q~H}H73YgP(} zPB$99YXw8Pb?-tUee@e*8?WM7sk62LAD{I|*G+?L0=*J=l}6+23Qf>v?4-{#qspjt zp>ca~K7{*&AKmhL&6&}f4vE?QsqeHtxG!#>kYhQOq;S;zjfeggVkGys_`! z53^fSr=Dk)cQHYcFG^RssOuX--My)yS-SmeJd-OWEylk4=KHc`#v;zksKL)L)tc@# z9{PfdzZrdgOdC#(#{7t?tKU(#r3y#8)2H2+H@S{TW|12A#7!cPMHEF_qgbNudqZXOm%@31Akup3ZDiA-gTH! z^aVQcfXfOLP<8Mz*EvCG-v(x`KCGKo^$M1>fLXJu%7>}8iX#IBL-F2%oNQVh_3{5} zSGb`~6`o7TYQPZ$J4#xCzQ7qgZ9&FurPLbFiYE7~e;XeGb0GHrdB5S4qDrveh^UVc zm~Fp@r#^wq`pXmSoRFAM68t@HvrXF(Hn?x$DX&91rj8}m*>WJc5=FgI3mcq%$p)84 zATvTc66yU!@6$2#OMiop4Pjt)+o-mQJv|-cLwj)=GaZVs%V1mBu5@+Le(Yb7|(JwM00YSTr(W{xjjdXT!&g1CGu)qHg8!rzNZML1b=$FZolFa z-KQjl^AFTKn2A~#^lCO8vL9W$-4J?vYodI=X1yl*a;TpjYR9#79*4o^rri=w`M0ES zq)XuZ8s}Pr)OL|wMf!u~a(`-uN2`vw(Vx|mVAeT|oCj2RGVB%biX|qb;-xbGVR%L$pyR}w8S z4Bjo=E;MlRO}+l@8})m)$)|D3xPFSmUEoNC^;6bP-c04H$@ZVibL-AqzRGO3Zh{JQ zozObM?*m9RKiZCjHoEtT9L#>-cG_Ih^K(g0xT`z$6JlxwiJ}xe7EId>Ogpm%@#lq5 z>e`1JgubcfUp6_WaxPwS@tsFytN=#aRo+nMk&Q{v%QVzzcv~3%ZTsqtILz(_>l0aK z(AVPAZ+v;=$wWM2WFj(lsqzV;k#ZE_E+V^<1Bq2OkJ;X8&@@&?a-j*aouUh z-$Br%G`@1DQ@cISS!Yh|y~u-weJDhsDm4#xJIjykS^st|P-V3mBIdDn=I~GSbXgaj z-Jf7Zy$YOMv$NYHrF}+y>&fQmN~MKEzm1yI*^IyvoL>{CAXn`zli$U<-%&=7)bS(Y z@6Xn5N2Ze&SHCqnzCHT&d+s6AfcZ=D{xx`kTTF%?cPAw%9@6GR} z{Sgg39+QfxeWCYF{4e{yil|(Rms>^dmSC0b{I=}CCUk4%z3kGN@$n1;gL=@b$x7k3 z(jUATiRij5=Q=V{@GXc3C!Z{luo#e6_J3^EP#7a(-qg*bv{LBUHT5 zq%rW$`R~_up>NtQFG96!=00zX7MbwKH%4-jufn`E4`@aQn_mx0-wRz4kUo4r0yz2~ zPmdN1tm6g~J{OI4XR9T0A9DskR92r18r3Us^MAEPq z1RbmZ_4)f{c2y>OQjx}20(7loVcB@&@c1A6cu2n*djGY&yAcoy4VhB{{gcU#E!;m; zGT?GV-~eWYK>`G4Qr3@NfVi?goe9?_(l2EJIUV8o620E>adRDsO}SCSV&}Z;_C^vq zqgNlj;o7p~K62h&>vG<(b@{t_x={#6*l@JBDsN)=O{Au#dFU}A&$N+F6=Z+QgnUA( zoc_mE*IU9bHOJmdzH=*WBkvMl1*eJ+S37R744Ch8tai?0qPV3E#uyPs{Z->DGv~hF zO&$r?+FuKcd8(xfJ(N)lN1Qa^^!SnvtiK_7-Z`#W!%wi|O>&_xz;a$7QKh9(<8doR zPgq$-kxw1no5_!GI~Ys1?uwL?ZOeL*KTz-SZj|7zID>>{hkm!xLmFm`(nIt+>?bsH zCHCQz>1%G6oh=(+aszN>-nuG(!th<3aeJvp$l{F)tOIYSvp?c+Wqi7Bqusd9FuOa;9+zs#oe(?9WBk#XLk`xG?~iL%AW6te990PBrBlz z^RdzqNZ0PpzK^b_L|GsFtKF)h=p(IgqmJ^yJ&0xxJ!}?eILWuvm`_?es@pwM*TaaJI=EZ`GS+a({eBl4BwN8`s6|Zvf2Ud>F3CZv^&y^l zc~XF-KCY7m8 zOG0N6_=t{{AoTV{7puqVpXvR@kY=SlKl`S1gj;i~POIGYIZ=gQ^P~-4RXEY5EXtGc zfGud%X^H^LQ0B^wO zTC4Ud$>~b75%udMjVzi|!&gT}@r%`d#rIv97q23^-n6FDj`h0WC96q9SK*H~e4VVt z-TszG4Nnk8u_u>GE8}IMs2mmJubuN?d*#Gs;}%O^xwoz~Aw3x-YY6!L*&}_mRqDof z9UQ2v+%NWj-IG5E>mGI}%#+Q4#Q1JamFlUTYT}Bzy%LxWb3GV)X`LQSOd>g8ARzD@ z-Dvd@GTE~rM`fNb=4817CK=`)Yim+sL8Uz|WY3T8Emz60dP}{v@y&-YNjUzf4{x)t zmr96R|JYeG31G8&q1r|A@2x?G7B9$ztJdwoTtipv3Xw-|SrT1|hb1;9b@&jpcWOy> zoNn!0oH`wn+jXJ-l0JRGVX^N6T8a!_s?VfC$$3fIn(s( zOYBKm=;U4M$`TNKhBo7n-Z!CVwG~=e)I$3dBN`R&*cQ+`VLyl%W4JJ^HVfuw;Q7-h zz7Fy^&FtJxQ$4FpNF|F@$|V$cr|X^5!C;*nIyLl>LoQ$ts8OQM3?3}2KBk#VxJ>-O zCsG3$y*=fler~@O2a>27d5M!lW{jX#Agu0sfdBX`5Pv6Mn;+xBDG$>64xLNGAzz({F7A9#e9+4*6M&b2e#37Vh)WZ zs6Hho+#`(&f*g1AVK_BL_RDjA;v%_XIYJ@bAEq+@#KNnP4jEPpe|Ul4)2S%Z*(h}N zLbi%R)bY4z$=6#`vqXCI#kh>?yk_#>qK+!b{P|!`rcnZ_V3=Wf#@>vl=eoaqxcCBJ z<(1cF&5IgpWp!^CuP)A*lRI9ZWIYbDOqF){5ldq6_qv<@{svL~`?V3jfb%mFw|EW0 z(-tNl--VT{Fj!rIwb{1kcGUXOzJZ-_xrJUdWL@Pf)%^O;1K*R)bk=jP!*JbFpHrdH znow8gEw9s`iv%%nx853auYMQ^W8b@%gTOFHh0P+*ME9f#s^rGwt#dPZ=+&zUrBTCm zV)szvqnUV=^t%TFP#D8>*57&8MO#LnVt1<6MUDq8%%=O zLqr-cFEhMI$KMoPzi7-?DzeLOxD~?0pgwh~*9im3MiP^u({~@K^6sLOE&sM*(2!@m zu)N%8i*uf;uB_{quXis zd{mKst7c0_k1vFG^)xQ^w}5uH&efhs<*%Sj{r<*__rM>&ZP`9I!PP!h;g&tzO^rsC=G07vmlK~XVNzkA_*dswEz@dQel#R} z!`oqd_lI9o*fq>h^F&g5#+8juWGnJO+Z`XtV<+CreZ%L=f{ZebSLIohi=VjLZKe}} zY`mP;6P-QA{s9+RZbeee)rxP~2;YD%o!xfyhr4;UAgX4@?VE4Ujp}&|v0<*RX3C^6 zB{0K*+oJyGN5lzw$2TV`Qwnq6*m4$&K>*Xb zaxbH7-jPB8;`A}|TNNy6`!;HaN9_@mqdklci%ED&QFZo}7M{bifVgsMhg#CNZ>Uu> zw&6==cdRy^+i)#kyFI&RXZx*_tc66jycmCdK@F_@37vHq(AYHJIM2-ozl00Me-Q;C zA}qgX%C0J1CpH;#i6i;ShsDZu%6&4O^&K;&B@o|8m+s4>;O&)#*)alNTU zNU&gqPDw+YLPN=fo_;?&UJo+?@_@Zm9`j&rd5|R(pBQ{zsd4DZ6V)jT#n4 zTzCEW#cWWIB*6{5@UznHfdrDkVMG7j_V|lM9*UYZVjbf+hvY#6yYon?aw_iXMbbHM z9FgNyOZ9F4_G8k!-yqlf)z|Z;p2|9QqsLYH?elAw>$NVmU6RX{sndHcx%E7@?G-Fj z?R!bEeEtY3U%Jsm`|8&Qhd=AJsqf?O^i6y@KCCtn2y)ry6Vcw%W5Db78(~tT%b|(> zsl9deJhAWJi2)dg|u?Pmmx6_OG%vgIW>Rv;eAAYKzW z`?6<@%n`=0NPidOxhI1Yzab)uBN}3QcNr4t?fIG5$syhZ&IITBtwZ5SjI``E1h0RA z@%BR-29vQi4utl_FnvCFJ|z-3Zf;x{%_^)T#(6Q zrkEEfJ)R|>Z^Lsik=Qa#WXJ|b)sJ{8+kWE3N{-ZSt69w*_Hlu-r|6i8u}Y!ToM;5b{UFFZ+*C_p+LDBKy@!dR28js7pKUMbQp#R8+s678wlSgi@RgLRX=$lXQ-9)Gafv~48s*oa`+8oVJB5s zK?j(M^xaPQ$Rv8gaO3u$q_{}ldo@i2X7u&<$z!qdDs==#Y7Lb2vU_Or)Pjx=o~@t% zDk_7NQpIrFPuG8p&BS$hrX@!oZ1AEt`c0ve#8HfX9?jf~#hAmrm-0+;Jou>{L5tLb zAoLE3*0xrfz&7JP#U{#yQlWBcS%^q)e%^x_>Ky`x_>yhtn2!_w&K0Y{zd~5O5SUY! z_osL9T6<$=eq#Fk&$dL_X|+tE<5*U&j%Cm3CmxPG0oEdzn*MoCJs8bgpmKc@H*4(E z)|kuw2)+0sO`vM$VnfHTk00|^I9kUqOD1jx`0@1(6-WKv=6mx6XO zLQIU=OT%p5%I8Du$Lbz2wPaBri8{D#mz-VpH|{9aXR0L%6Hv!4-o+fRekg&d+MjgD zT>c%168|}Cdr!yjXo!|)_fvY&I{U#os}hlf&GuPsgV$lXQBgYkpf@@ObcDdDJSi7h zoL*iXzpLTY>mS=^iQqw|V5&RaKX$L!!E|ZS27Ko$SUz5#AKVi8NYfb_8%uAbAb!d{ zfYJ{6Rl7%y>fQOm4Nn++x2wZU!`f!lo)KqK#U% zoNINv)%Gt8V%F$o#%4YEeg3zv~~I_utN`l7Ex5_yVhqhzucj2oon5Lk3u{Z0d>eShSX4o#%XQ9Sn~+00tq`?c#W zP`^hbRr$;}z7kav|H2IJ7I-z%I1u_B!V?AO_j!FB7_>UWL#~+`bpBBjcsa0-1cpUY%Q55GS z%J2Al3YMgLqpG4dh;0*<0;2X;|CB#_&NA9BNn4YUtQ3^)c4$q9%lNo-N9GiI8$@>*}vaqkvJ0qI7Yzq3{>dO(Nd+g142}xaPy> z|6x}Yp!jL4i}n72?{mPeg>PSl6F(5+FU+%i6Tv3Q@gX$+BC>8wK8=P?+ciMTHF$ln z<@Qmu2c_Vs!I_iS1D61C^^S=mPRv0iHGe_|&qZ)dP?hxh_}`PxfczZm;I9fO?&`8( zcAOF4>Mojfz0QA2`6?1dUUN^&0Z>tPi>+r(+SlSNrq*{vjda78E^i&x%bS>=y5|a$ zfLh>C2U!_CcSQw#EvLF-G2+5d>M!+ND=5L-?I@WLd_^qZ3X$X=!bdl@OwOEy&MN^8 zajIQTi81h)P-|&Wsk2i)&9XQ7htp@tbX7*g=H}IRb_Yh@8O?MTSfVI^B*+*qdOAp4 zwjB#5SZw_7&=pDgJY~c+Vt{gZCu<)WQ+klte|8lcRC?Gw-ROabcuhRQq`{b%`cuNs z7Pqn_DZqmZQ@j4haI!ZEUEqT2d78R-^ewcp-nRc)^oJx^U7pKg`RFcvu2j9h$)Rvf zUWB``>LVJp>TtGPz+ukl}K2f0;=E_aPKJ1HGCxXR$Ha!2Ys$5UaMaOfts}HUsNn0 zejtr86m#A|+}y(bEdN2(Ms#A>FgA_P;}?j!(pO7Gs|q#4^9l>9E<${TG(wLy2_$T0 z8j`rpLN3WcqI0x*OusMjuy(N`^~bs(h&&%ApZkvYNo}X{Z#qX^ReuC22|Sih6!8nU zIAQZ`%Kc3Em@$g=>X5N-^Y<$GrIQ78rWub}6RjBk6lNpWo}O_o?piG;5VzSJL9V)TaF z;+HMo7uA~2Bwa2(sa4vI+SMAuRD<=lOrpb&fqxhET$ioEUmM-&A#i-E5kScG##S8~ zyL{H=0-BG$9k+vT;TAtAOoH`f@5#+iH!473Q*UqO>pr+s`57wg0bSU{41A^9D8mIp zv+&!7l$W7rF&%X(^5-yyuoY}vZ?C$76GZ8G5Eg3#5v9{eAzS6|``?@vbC?n9V|=b-zO$#B;ep5eLr$JUnyh>&a)7a~b-8 zktAw_1~YMiV5xD-JKMPMo$F}B$X{sB%M6hcOPD3q9HS7=N%Ac15$CUK3Te86)Nk*M z8_oGU<xdUbzLPIwM|ibRO&Q~A3J&XL3`JH>bsjPt9d>Z^n^PzcW{*L3xi~vh!G6?xjd9NL&D(Lf^GB z_vo9ZzTlYI>Tmn5r(ZNKeK3UyM2gk8v1KORGs>xTzKWJkiqdCJH?YFUxp0eOG5nlE z<8m&fZz&pma5gJ-IW{x#a&6V<_0?*}8|zA=+35==Fiz?6dSe3y zR9AZFV6($>6Q9E(FJ8gL;OtcQCroASl>C^r=5$X^wXYZ*jn#p_bx|3C6LseI_3|Na zvieUJCJMKFWxw%c2oj2%%PlZa3YrTq`_PlWA^^OkS%VS)-E z!=cLpyfG9MB`^1_eM+cN{u)?Ij~eqBMKd6kwxcc`TWT(*iqIKfx5_Ftc|VxV5&L6k zp_FbLj%0s|$cO&Qw?L)$9GYdqn8bz!b&W1UtFsLX3XlI$-0}EeV81FXA z&=5R+rrLB}j;eH@8nxH2VG$m1(fI1{xoyfp-zmNWTiJuz+=%>E_byPjo)Q7_4sSil z!6l-5=Z*XoW=LZ0Ud;H(iRGOb2oDZ6l$dh#?rpM_d$gwT&{vQZ&MG@zt44(qxTrp7 z1I5su5QQ74S#j&OVFR!_yLrLdOkJ=^D!;;kvM&G;4=%-7LFiF94e61m+x%9|%G~lH zz2)|FV?@0OBu|wKpwI^Dx1p7tgoi==5_Ie8i`F6?pP~Po6Cb0pHA60 zZP`i@paZd9TEdtT27031m0RGt#?YDP(SQOyjitIWF@l0-1&tp;hUFT0&xwx&~f zp`l1dukuP;bCrDzYh#*dY!a)Em2ysv;ky@fW)K49cr~OlhNVgiwd?dmYohV!`#?!q z-ad}Cf#*<#6OV35$%$&R%hhMrz|P7dwh``-p&+83YoEeWeT;%N*nN33{#@y4nG!-VKt(75!^2= z)8xy;eRnOyLk>OY*W!f6GpZfxhq}P>8AR2=D~max26Q7v4l+ZfBbFhMJr_+nMFZR7#g)t=EENhn-X7vX8h}crj(V}}K)c#)l>jBq^;&Edu zcqUeiSRXVi$vu+==H-|R>2^Y&)(nsqc-v4`{*_cl(_b^7G|)E>Uj=lN-Vb?28T4Nl zh+TcP0z|uT7h)s}5v8FB^?&Y{9#=V!?fT_I@)M-=Rw%Cf)~e@_IKE2+aj2wow)=$AR(BJ9!eTMGD=6NO}w@Hr{%UpMgx zF7o_su^auN3B{?JK40R@qgvOxlOdB@=hgf|g7Cv2{f-EliR`yv zRy`CnYVu@%1t^-5{YWQCM zgyk#T14_|_Fty<)T`$t6Lor9Lg3A*JL zyF#O0zAHOTSKer$8A{cdpJ39wb7CO)7=0k0d8S*lT6^a^;3=9Q&a#7b{Ip%=xE# zazAqd^&L!WXpwY5oL70Tj*0htHRBrhKx?ykOwn1>Vwu7ePJFTERNy`vl;fun;!IN6`K01CJWh(9ek6q)m!(0!S z4vuu3)=lx*qdAa1#VwxH&k@YKk(Y|W>ay*o&qD44sm@?iKmCVYn6Bx^SJ0#1I>RCG z_-rD-_~&bMJI3w3x~h+$6-V^A#U-LrMZK+@kiapNH;PDg% zh&?;;l;iv1%$PKsAEd8~Kc)Q;gg@mVh<`EcATD%brG{nGiaRz1!l&3uNT=+vHiq;U zA4ah1=PXYA+;sCxg1&uA!-B`4T+NyIq1jRm-&LRctPNNJCtpg!|Jb(vN(b=!LoKZB z)o|Ovl$R0(1DP>zAHb}SDiCA_+@mH=n zfEa5tc0Y2X^6fG?H%>SwXhEpJ82)X<{i*_z?eWpnc*L0~f0>Egts*9T|gQ8xx2c_rU`OUOmZ{E~7V|MYUp%FB5h*;<)@T$>*?FNM09*r+r@_ z`W1~R%f%BlqW9|IfC!tzZ4R(s)-pJ^y-M@sLE*k4n_H2!s&3S&sNG1e`ATuvVOoSY zTVkdp%_UB(#V>t+(TeulDx4pflRMZ`MU9I)i$)6kietv$ME^%Ff2O4bx^0geMQv_+ z2`>+s;!XkMGzN!hbyfqQ=^czqeHiMgS)RlRPQwDr!j%NwtDMG~-7S;wIy&;QmA85` zYB%RGf7#rfqu8d&mKpG*%8zCmkH)#)f@i~0%3ZiJ9zW^BoB}h5f0%{kH4cn;>DQ+? zFEt2u8Uhl}4??davAjvh)T0%tLB~HGLUET^)yK*nmS1vnzMP>mMwLrf(o2c&Z=ikK zJmc6R$ab#L-Q(k~c313(Vi<3v{~z!@vMB(#Kb@v|w_kw;5b?!!WRlNk-8px_clsV= zL1{rgxlGNRyZ2_*dG*n4(Zj~*Y24rpO%Rp~&DVLlK!H2%`s0oXRQt%F7@~BP$aX_M z;L}6buRa+*;e=2iMGW+7AQG$LEer}euKm-~r4MC%#)sSq^VHGpIij=^pVpLuH25kUNv(E05L5z?Qy#kb8%yZ#k* z5zBu$0bHH{oW`yWxtJ$X=Nk{a)binmZE3J)#wABH=N|h}JqN^AhP{^htBnbm+L{NE%)n#h3&J@`BWi1kO;(|SPhMBn`{ay&&a=5 zCPz8UH6wH%`272|eLeT~lDFJf`uO^MOw0Qc7lvO)hlI|LZ>g1-lrPkx5G}_>rVY@;BzhZI(D~ zwpIFIGfwuLy^bhWHI#G?1Eeu5(#kO24wkv_1$6kxyY)Zz@oKQTH>o*@Qv!$lOLF7h zJ0aYdE+2YD^u7eNiXB*K^n{hndclDmgff~+PaUEy7ziH3>C`NyL9 zf}e6b;T2;9`sC15PsgAS7%_tBxEz!?>hjW83+9|c(nO-fps$?sAmNdtsreKyuchS! zlNPqyWWC$SRh%dcIULV_8(X!n&ffRCI-_`amBSs1P$mn}1^8d3d3N^XWPjPVssqKB zJ%|T{Hmbx>>i8<$d(WVn!(vGIbHsbyM>$2ns`>pZoVHbFl!_@_7kckD1W^p~7*RL0 z-3`t41zZj-pGk)p`usi{rH{r+8vd}e+O*ITlNCuwIEeWlt1k)U8V98Wu@~XDn>oqm zT%ju>B_w|(4jWtvY!caegadm8iWw0ekxBq+8Zbdr_{ulxV8%XGk+qrSBcTzG&VgUl zFP}=g>(6f*f^$j@+*nJ|8AMo=4%m4R(f2p9w zwz!z_lns4CC{Q0wNielVt#~-EW^UsySR_n^dRiAwvPkY z;frkcvY&Y8G{c{i(BswM9E6yKQs$$pFzyjr|Q3LK7|0z7xcUe_hcWcWXVD~6ud#tIIT`)Dhv{%_y8X{2#Q44>u2=$JdoKD{{Xvm@%wWq zhbm;)oi`N7z_!bgz!HF)o=Y7EY7otafxmahSqXnNj0Oh97jlyFJ5YdA%UIfYm5|Wn-0vqxd z`hPujFcr$pHfCc-ut}A)V>dxnw9$2-PZ7F=#jz#0LsuOd2;C@kJl|S;)woE%ydg!( zc#;3w3mSM|=M%(f_l2yH#W#F@0uM4od^QtPQrX9EAiH&?Lh?KSmP~NmFq+#B1<6op zj(SQ^<;VY;wNQ=lS4E$ISo4+!nk6@cG7lY=Nbzfc?7p=bek3|G1+fEl4ip@d9Z=oF z-gpef9{E9;<5w?yHck(jLfCJ0O2-?CzfLy85#KZanlb7Huw3Ag1 zp7Rg*z=h3a)!9mbj2id5BidkKY~_ zW!*!Fw)q)h^W;J(_*Bq%xMR|aG{D0Mdm8z?@-e`8&z%<)+{>58vOsJZ#8MV4Hp2ej z#oA9uTrXorDx=7jmGm-C)kFiw2=EH?hm%;oxF zQ@=!>&Lgv#KmKW|$fw$OfFDve6I5Maq0|UAq5c9adkb10S&D8Byv3Zz^Uuk4Na z$R2S#9h8GW5m_MWaDQ(%8!1uV^m%z+?X#rczt*)K4%3vh>CZ%3)ye-B>zEW^?J8t@ zw;!KvDMbTyv~0QTXCa4`!_7EMS&uB{sKjsTku&k;!}vHfO!OfaFs>t|Cdeb={y#P_ zWmJ>5sJm4N*0J|Ooc4h%Igw2qbV!!Us*WX`7t=keI?zw-vq*Ncz-clLlJwl&D~#j_ z(UTdfVp~(kN!p5f%n|vD6!CC>+BIHU3z>QZ4)bVh9&eBaKk+J|4DT2WKK321N)pe<5YCF>7i+^q!i#i=5VXxElAl;9NZo(;=iG0` zUKQs2l*0PzSNjDclun8O1+yXuE_O@%v_M1~1!2?Pyxrc28L1O#pxvvz&WQ% z59NR`8bbp>@H(g~!E5;g!;<7I^pJhIYfT)RL7SG(u9XI%`1S;Jww^AqTRcN|%8{w^ z5GRFsUEJ+aw;hl4U&5p0)D8mkCd6uJVTM=_n_*F`-`FsuPx(mAzeYn?hazF`LJ*X^ zYUomEwACknIt+QP>@?A9pt?(;b;$FE`jA7t-jeWPis}aX`F9(K=WZfqaGNb^?8x8w zoLyBW;EEG{WleTTe^w7`4{Qx%zinh!_gB;zvof+F@m#ayH8I{!1*6Jzya3n&O{V8k z6hH}^`_ZBiMh)HfK(0UFXGeKpZ2nr6zzFa44YR2CvPLk1LM{OpR*=}`z@NC;RhZAgnG zqmEQsv4X>R(_2cc&*iWF0fpd4uwr36X}?sa>LB}y-K0PR!MnWj4cEc#Rx!fAV--|m z@0C9=iAE+xfPEw_Dk}n1O{CO7)s!fUK2?9bF0B4R;|^M5OOqVM^dQ#Fxq3Hn{!`On z-gw2E)w2-21epmbm7mj{C0|M!Hnp6MrMfyK4N63 z&yIJMpmdM_ncmzCnT4OYN*)G2DAR|=;?cLb!ZY`I(O7AmY|UMM+|eR)a`-#n+tbpC zY)adPcCbyJn?VxkYZXB9L@sU(1I~>|;3Zx!T zp-V-JO@6#Mq;3PQq^-RKNOJ{D*&TG}ua4B_jo&V3_zwA4^6@Xhl?!`aSN9y;#QMP1 z1CVG0kSbiyaqcDl&p7rG;_ZO8qs?Vx=Jo#r)=V@D&HfxP+=otJk~S;zXNZ03dLkxy zNa_@;FEo%S=KUn5coTuZM>=c!txOGUQz3=f{?`Fx3CM^W^Gx%)47^W{a@ynxFMt72 z_0L&OX4H0yb<4!X_oS-&8tXz6sI`}bmAHNtz&RA+#q|BmAV z5NpU{N5}VMvQjQ_s~Y|1vJi=Z(755CzM2GBfxEVs?7p$tUIf}X1>pcWc&{)fG9bT) zrSKT~^Ea*F55Jcc__xO9)~yLq8D<^7m?l}$GdLjA#!z|S2?41m!w(pRwSY4iDC_QM zj?=Blt2;$ryhzq%QA*&jr;dsbgIxskYCt&vL&HCNm6{dvp60da8l3EL#{KAURp9Lh zGo_FDk7b}7>woPsTBy1M>f_I_&<1`&C6tZD|NBz!Kvr)|NsDnqcroueP1&Ila}q3S z@hA7a2*LS`e4v4TiGuu(<@{+&cH=0zUk|m0tBYKxi&1HMUcIpM%N*#9KltuYGJJnmHq;aQn-B2-SXzsn_<5Jn@_v9 zVniN+jR{F(V{88DM;GbDtOHHC|OCfLyv8 zj5|<>g2(SX(*l_ZN1#w5ghD9{OJ4Pb>}PYsgTp-s(nLD^k%v7NHq4b|$?~40OHd7X zCTp6s8`}9M8`p&cWfTmWQoCY%=?_RqL6I!r+&X#?Ae}uBVfdJvV0EMW8Lai3wFWM$ zHFgCebGB4gAjQ}eiGxTSj>sxne4F4?ao@*vSiSuirY`pI4615zlQ|pSWX`P(Zp;rk zP!G6TwW18EZWgGOfu-~voXx&xQ4f6(8Tbh|5&PC-AM`aBCMrbzipcBdm!VJCCPssP zYLU~dzBZNbw-O-EJ_y23yeBOKOqs~{e8{n3Mc*knAt?5 z>zsy#;_RfD;UCW!6K#TnX2rZKkH(SgpDRTV(eSjpS3xmX?^wuypl~i>jQx z-O`Q6Wp|V20^?JTwBIT{zad(Icj65hMA=VID844o{bWM)4s)p&5!;K$K6nRQu>kTy zN+L+h$xAAEM_m9I!~k1P0m%oHxdHKfpfZQusfR1f9RN0dOzm@7a>luMSnN~8n%!gY znbS>>E9t4t9i$k}W6%#c8KNqF0sKU0gzGRaWGf>7@e|RY8cQLPGKOZz{lg}Y1pRYD zdDo7V#lfvvB6ClY&+}H5FVfgBz*6XAaArzVbqB2UIneCVMTp`*mLaE!hXHpN`wt3w zL_-GeOJ-GJQ@QGpz^5sQk5?<$!NnG7J_|VUk+6*)RSUDS%%M%!F?1(w{|3NbGIR7J zMkllR+AP0UPlOOc5ri?kCjpva|FwK#j$hy*VLZ|m4Bv4b9;9INuDd^7W4Cib2WPNh zpkO6HQU%-yK;IsEZ!-Jqe-~FDBmJoEKFwy6ZGHNOGG*#JEP4gfL|Ocv`>CsZg2Ij0 z#_h|5_XH1HIv%MjL0zQiJ?dbOKC}|C-8KTisIvWO`#2jWv&lMGX2O;V7axv=VE^U@ z42|UqBOgw`($9djd~SE!H<1UP(q`?w|8w^+N8ln`tGqJM2dwvAD{sqU6h64~WOBh& zqwaCu2{;>n-u{PqFbm6ZoH;q6wE8FiG?ez_egy~unHwL8s* z){a9tB5>mk;mH5qNw?TAUm0}!ez%d%4Hcl}nH>Pi5B2+l83g`Ixc!h;o78Uv;M4^; z)8+#52}N26HmNF9`o9MK@ZS-ob-< z{SK?iSr%jnPf@Erca2(_Lo3>Kjkf2|6~2Y&@FK&S@G zJ>X{b^CN2qD2_6jf|XSYfB|59NKY z3E<21xK*7QkM;i10ux5Ui;p28?~H1V^GuWrRO0&bv<2p$+~MiT>DN&GD!%%&TYOc= zWo6*y?;n40NJg@7cK{EeXwxIyC-zqTDbD;gN9YU?yRDkoj}}h%8GWhWz7)Eg${GF+ zwB*@mdcJEvjK3Ky^Xt^fTp8)Pos+nIjJ>{yO(bC|a9R{6fpYY~p%g)P;!PPu@1270 zOc|{Lv*E%+Hvj*98`*K;14VQaOLQk}_$NJ&ff8}H3Rdxi*KWO8n`Z0^dfEQcgZ4lY zdhopc&}L;9pO%XJL5Q1SbyOhhKohIB$+>lRg<>6C$ukN;`-E!J-WV9P{3r9xg2!{# z>1Cz_TilBGq(sUnQW~Kb4fD`n<3Y%c-(YO_F{pvm)_qY;Ye>i0t6~P%35NGV>l~9 zW;?z8$G}Y50xaw!k$%KxU+tEt z%LS6;qF=g5!=uDwk;O>E`2)-O=3r@%yM9>&qX2MP;J>j0c-xOGli04i{8fTgvr+;X z&NeG=_#0^aTr!eTW~{;6vtFU1Bb7EYZ)>T5$}-hE)>!mtGpf+^20lTZ?eB8*)&Z_# zXN?$KGzQCQ4$uEnyNa4373z)*$hV(yUyQkDvSEPv*Z?&F0dYv=L;YvWPfO4rh5md< zdx%8vrr%Vl9AtBgZ#Y&)>vooL=3=&!Y?Kh)UswJH>nf1(0!32h#@JM;>j##cN7ge0D9sE2_&nk0(`k3#eTJ=trov!6q zGj%^RHiSyMIa^k4jRKv#;`xzIE*ABsuJ1N&HF2a&0!_@&GC%j`+JNGz}3)B!+YH{s&X;SCpip~m$d(ucpb+NXaC zF20}4s~8q3bsm3a84O0=+S)t{qxBg#k4u)+6QF74UVP|7)VTcb?>oE|mdZZG68(T^PA~kZyL-@cAB)GwJik@qKqz9H|4&gB> z=n-A1|5cD1sVvYQ1=jFE70Koj0_d4GT>Gyti{gmrcJIV-7nTFZ+t4iUwNTpz38N%{@3s4L7aFQf5nusxV%DsQ1d))_QGX%jAufjJnTF zzfe_yr4z6Ci6+QD%^G)M`7yJAHJ;0a-=@|yVGjzbVY-W96j-bBxzyJeeK$c_y24}PzSr52p?TWa0RCTu9_42IW|iILEvY1# z^NR(v-z8}IgFzX`?sNSV0!vaY#TqA@j%MYM{$!a+-e(=0MMjlRbkgsps+uCcx5Fe| zjlb%#9UVVw^L)3{eRg;NrQu8IH-FlAU;kuP(nwO{|KjZ}qpIB2KTy`98|eEzmXunkZOd;+$x$voR$Vl9y0HU%&$n)A(#*a+Gb) z6y}^i209Z;5(-jiy%lt4B8DGPemi1T&MPu#e5pus$LeWstUZJoFnHE-E}e1yjOBwM zmRIXQf`aFFw#_es*Xf>_P(x;Ft?|bNCCgimsIEmcGF_U$Yj*DqLY+b$_~ zY6o{-_~{Y2-4B;f+XajT6F8Ob@X}RgT_*_Vh(2HI+SwaXf*mBWx|)5j^mdkm?6B4x zPv*Iga?;?#4y-ZQvQz5!(6mf!A8Ia44Wd*o#`i!sDty=wvq6PCoSh8dl1kwbcsmso zkrTlm0;|guWhntK1k@KC5G|+-7g^4HHdei%0fhQysyVz}K0THlGJ?2k%aIUa>AAZj zh4Um`rhoFPafkVejL}kWmSdKPbjTuGpVQ~8VE4*`X%4_S-`|@3L+b|% zr-t#6y>V}JQ4edIj&mP?WyxNp$OW>@ai}=p(_EePtib?a=4PNl3PMr9T>`i2Pt@Ai z5ko6gv+m2z-yY{2i3IicEO6V4s~`@pK=fiUxj0#Zr97Y+hc{rE8t#dnKKmltBcK4H zpkt~B3IG!1_-;=zJ(94^Fpln3q$JC7o8VfdU&nV-dPyk6wvmEvyOICqZzFBoX`V*( z#)3XjgbNSZH2v@F`fb2Gm-UYy)}~qz;J7l4B(`BkNFSGm32zX4N?+7qXum$X+0rnv zc$Udcp`!@~b3w#BIzUr$i5ynY`nnJc6trze$bDHq&ni-@py=n&fLYArYKQ&dv??z1 zS(FLVs^|Q@%xU4OxffS2-(}|B?7OX;zyi&jwjAE_$#zyzI&g%3|GEDxU@4*fa2i0<@ltL z)k-a-2F&*gpRbG1dMWiB5@|h)OqR4h+H`kqe`Zjx8=QMl=(B5dO4%+QIa%XqSml}k z(5@DYREz>j?f@hlG%uuovxWWL9m7T`F9ia(ClaxRSsqLD`b*06%{zm>w@!yo?lkpd z*LQ7&QU0jHakWU^|Fit#Yau71qE1mvU7~{BmJkXP|ME#D4HeA}%rO*=^xgh=HNrTy zxOdiBokTT3SF2D3n2}W9(IKA$2n--!)Grv*M0O*<_M!5)D)KY1=(AGAP=*JezqAlt zYCi7vxL`nK=)vopOqGtB9M%J95d^|Rnt9&UpGfXif@VO(S(Z#%Vqe>%ux zcKYDC%=4_4#o9}w$_HP7W|(0b_mt!pdI7wikl+b010e*lKE>C*^jB zt{q^aT+*bl{4Rew2ePbfUXGl)wn*O}Ma~>wCMIY2&&ZqipUxgs4T0aKNj*7bwafZE ztWFSM*nMxeY?jo`f|2QBd!F?6>7h9wNA`R40$T8^Or{fic}evOKuw)+N0Yg$f`LMb zYT3s42Zr7NKdGsD8_(sQS-s$2ymiwTRquG0`lZw%GxeUD%K}H(-sb`1->_FB6<8!I z%KQ`7{SMc(k|!I!FCLgCnPj3CwhwP;6RwUPHtFUSEIASz-1@oD#qy+agB&&F%hbap zw?`aL85zIjID3)NZym`}Cg0bhQU{XwQj-=X+!yZ$?JjSGE0 z8#877Wp;mACt0cmE`ENzf^8Cdb`U5uHc7$6{61*c9eEn|tuHB4-jERN^12`t{bq|^ zfo8*objqr@%;h;rUT5q)tJZy&Nx#Kch68#5&+-EZ3?go%+Q25NwOg;vc>PG#Jx>xP z;Go^7`B8BAzGRXs>Dv6S_kIIW-ZONa>iIzEWM=x(f_m6et2rJ^+3`oRx0iZMQhzIejUi%wZvRf z*G#%J#m$DnAenN36W*=+OelI2miM2UCp79WR5p?8G)}WyBMr^XQvo)+-Di6-k?U0S zTercq&TtEX5AdvOJ^%tefCoBJtCJSZ4`1w9MA4n_A=W;h1V1!WuxC<*18okg9R6I4 z!F0pXjYMd%HtN-m17gZPoPx2w9ja0N5YqDtA!EsV7fSSy1~y+!1N%4#aUk%&byY?| z_G%GRHvjMLHD12Lm>{})4SZl`K3GzFv)O|(C=*JQ&W>N_VD*8}Bp1WcUTL$~_&RXF z$g(!r#WVe@dUx(G9B8xaWwJ-lsS1j+8Zd|PhyC~wR(Z$orz(zyN3+Be zP@GyQF?PK2v@g{MMS8)9T?liBLmw`o?(jv1^Z8an4k z`l&=#TWW4F&p|MmbJ|3L&i#>AzMo(7mTlw;yr7aD%JEn!7he?v8hy{=@0?p^ZBA4H zr4T=Fi8f4!Dr%TkML8}2t0yqaCMNgsCa@TK8=cNt5;2ZtUCc+a1ZFb_8CT?FJTd&d z#r;2W`J?&Xw;S%L_`b`$$1jWQ9$*x1K?Pr48;dJQbZON++LVJjMSwx3Q z;VhH1vbwEi+;LO!o7cT^(sy`^Q(opJK3ZD@DwgNT7b8Yzbt~9q&3humao)3mk(LTj z4PL~6`bP~{@-1qT(YA|+;2W=($@5FvjHb1wISl4{8q6Bs-M@Kj*X>AsMQ%{X$-8D+ zItt*WU%HU))St*A!st$XHz60r?f?azzDx;w@T<^j?}qgw@u@yNnxAlGvbkS_@WW3@ zv1G8^SNLvm8kEo!SuoPjNW!WI8D~ZaRKrKgfE?EGkt29ekO3&o?5Kf6^LOh;R~eFT zXyBBlj@JJdj+zm7lDcB=s5{D3~0W+@fHX-qJMZ9d$N)JN?S+$@e3wmhH1}B(Yyd ze3n4^tzobJfmPNzw<~<)b2Gl$H;`ls1(4Ue}2&KEt-P$(l`aag8BbgVBl?5TP-WJA9UYkcF8?}uLIs1zg z+A{eRKpL~kK~v7WQYbnnx$Uy7H-gyz<%ORv;B0h`l}TOrUYtu-8=O7*P(!r7gF2-t zSgET%FmRbH`ZbdAhL%havA^7#M|$-bhhnl;Ke4QJzh|;v(W>pW8&`SlgBg^%;IkPI zKyK49?LOK>%67hwG{#PC4Kq43&%XFH@J{`F@rCn1nSEYfEvy}SSosc_zF4C4zdwqm>- z2n`GWvHwAdg|ul4+#oq&Z?{v{vW7%#mBM#1QgxGb;UA9pObvxU1T1W{;C{0@;5m19 z=?wOQjF5UZT=^0MHS=(LIbvNo3s~&#cT8(sN~e0&;-##6A{+4@+axXCQx`m4o**WC zLu6P&DJIU-4t{sSog|0t_1VT>iShpHH{Am*^(s`$loK*X9U$>ezBBBy;u`6vZ2UPl z9}La;moQ%**j=`|brFfx%oXRsyRJXQHnP09%e3#5Uk!A2dL1q`-+iLGvA;UqChKo5 zxcrswZek6_`gGd?RIEsjt?`NwO~ z!vU=nMu}!a!t!d%K)xIN!tP3VJ3p;!4jurB9WCY!E0V~FzK+!AN$iNjg7||K_rQ(C z>_e;G$l(hMWCL2x$KU()RDREe?CH2}w0FRJyC^}#y(iibO5waI-;KWay}9PX{Ulaa zZ6@gZoz0s+8bkUFWyz_Z{{4r^*)R~VH{^QUf-8aKf6;?23@v7YpBqPqeA()kr6wqs zn=szO;#^pqZu=?lpq3t$IbkabX4|N>6zn$U8oh!k0nI>(8(jrK-^3c02OqKF?WEk* zQFQ0{l{w=ki6YoY=)R8lIWuRZ3V(0CJ!{ED!-*=jU=H*;gV3FhANC%IwD`-IzMS>F z;*JWN>ltKylJHr8={D0nDxcTTiCAXQyXM$ZQg#*spsVU8mM8nEwEoR zrj|il{Ci7NEc`0eQv}vW%9bj!P&|EM0PEiw&oo-v>rST^M2RgSk1*27#4}hfMX7Rxzx)EG?l`&qJ?AUV1 z&b>k`JU66!=z$>!pOQgWrRaJY!&eA_yj+$JvndvYS-fZ3;6Ae<-AxnTz$DJDopHOY z>D}CTpaGnT+4z|P#*cyI)~PBOwYxa?J*chm6ayp3ha_v`_FO`=u0A3{rrpq*_4?A% z#%}c&R)ofNU;Uy>qLdkIuocED`JAEL-vDGbeVuzJ-qcYMxfFDV_1TZ zl@>&6B8G59F}uAqYV3G)`?RNNdk6R7T$Nz?`gy+!OIjJX%3ro=B|-TVKI5d%!z<(1 zsW^Pw)bY+Q;^UdSIB3XJb>5EXH9FwbrQp1|wS=9WiQm5Wx9 z30o=BA*YvDQD+!>fz(iqv5zhxm4XgeAqFITaWwWoo}U9k(wq`S1%nP5>QO>Cg{FA^ z89G7gpirIyCjmnoW-<+6M~5Dcy~jEc>^&MOGi$YddXA2yuhrnr%W&p}7Z2L74s=5{ z2m3~;gqYb~@PI}b|0K-|l*@#aeEs2-g#-`ji_ihG{!dUD8|3WUgKt2*wN$d);Y)1Y zlR&U({Voc}nm3Mvea_b$a~%m+m0h=3pv0njVbrj|Q{H{YTZ6}n(NuG&NUwfDFnfm)3pbX`=A;dt#s-~2|2K|9A65lI=C*)hKR(FST4)lU;$ZLsb*`&P;roCrd zhMErC`>6w+?D~OVA=XT7OJXe{%N1fMUkN}|j#V>4Ggw2Ez@<9Y;y;QyRxu=+r@4`b zG3dXC`|x9Lt9o(OY3}127ZWkhM)mQggXxtF(7f){8~T|#&r>jhFHXBc9?%Hfm>K*K zf@-N8H3x}b1@13P@jM zv4g?~v~!#uhiuaIS<3)RBcRo)c%;5DQ%8?ef?!$x62zbjZ#u45))y{xT;l2DLkxQ| z9IOl0mv;bJuTmpXl@xV3ti}iX^XsJvr3@PK;&Wsx@l0*94g+|l+j@5KhJxOa3I&Aa z>AGtl&su1z?$H(>`}}p>U~nLTk3xA{D!0I#yd z;x&L<`Y)6%T{YUA5HJ=t1+hM@p!3~r;7<9o zGv;TljT$uVJPPpZEbQi3>yh$}w`Pg1Q^G#B;U%l{(+NHmbNHsRcGU=QCN+fBH&>la zTf8HGSa|jB*tg;tWME|d0Q3s*t%44=XhoQ0uqC?}KRZFBR9n%~5aIJYb@$~}#P^XOClkh(3WR7UitmP+@8!ywfA6pu zKE(D~v})CPb@|n7l=j5y~`!vpUw*g4{Di9<^N*Q)~J}4t>sAEm`Wxq6eSqlm}_F z6@t@2k6&%h6O+H=xLiID*^7)DsWL5%B6a z-IO0_W8v@eWnf2%c-mL1sa}U_9I))@021l9NmkE>%Bn`u9pYTXOu4a zF0i2)M3^5}-2%3UZUa9Q=OC|}CHpfyT@hTMfdA$p}5 z5}guzphWtY48%wO^daio-0i-dv=@mOraw-)(A~S%~~Kn&)5FLUzndtpCs@Z){btl(V-RQV+W zx3DM?^VoMq{2pa5kcF9{Mc$tN>D<=5Owyu+in{ERkEJ{Nwo&PQ%rY}_oHMgmhwi}t z(VNeRR7l9$IRb_q1L{xmEG-)!#LMq~A}nt`f7OaAOGw*4ARN+~n}{jC{*FF{yIH~7S(y{;Hh*_tJC6BWgvj}kN$aG&$`>zV6Tkl+>XT?F2Iva zl|n5zmWo}f-C{e7mv$|YEsyMl7${x0n~3cijZQ%Uc!^<&aZ#oS{&nC%*QLj~v+MKQ zkz+`wX6kJ<4)~pL$we1ms?C=@Iq9g<2Kr_?pSrmNds4;X5=`FqJ_dT;j8bMP`;4$| zAgj?Oez5H&(H7ds9gp9s2G!7bb91qBJ9YLcrdXH1n$4kJ-HcyMFh+se5ztrDNI&ZG z{xlg+C(2{eLmlE}HCAihlHM|XMaX&L{%+=S0-dNyRNS)6uXpU$^I}W+txBM*|C(0T zxW?EVdCM%N$h7Tpp6GP6Iw8A6Rvmjos0PX}KDg{?ll@jv3LNKV$mpV*j0QZ1GGvM8?$rSJ0 z)>jD9#7%S$BkD^n5%yii`Y4-!sdR5MWo5E#I}S`O2s!3NLv|>`ow}o4q+kiyI#W2V zGYWGZ`7hqLM{pBZM22i7aRC)nNGLbg1uNA8O++nrw1QM=t-^TJTyEHB9;_G(DE~e9 zOKr@E)vnI0p^b-zqlXHDB}WxQ{i^`{ga>js%r3S0Msr>J;FG|g84f) zg2(4nUf0=8v{F<0%M}NkZz$OA7qrlq8xJBsK?vJJ1U4Z{yB*iQ#R*HV6>XLPfj7QE zb=W85$-;Rx)!du?r39|}r*%S*qx_Z#;H89!O)s&cCSCH$ha62pu_f->J|Dgsi?XGh z)1xalrVuGqEhP9_wyGc!{~SVJCC0g(p-|a*z>-igs|3*k3eu1)e5V5LhxRbqda@w_gLA{k7C*7PeQ^Gs zu_6&eWvspjBKKz^VD}b-LYW!z&zOpj{TuQaW7o@MEsq~CL3y_%Rw-8Agrz{kpkyt6;^B+QBKe}2_@>HFg~)cTSERL= zika74B&uY&GRWAIs<~Jl4b&aSzkzr7gb33XVl0I2J%Cs*m?7ef3!i#^m)3BH|$d z2-)Xzhi^E+Qu1@5T#4`7m$(^MgvQ?QwR^!<#@rDYx(L>9_(beF(| zm2z8EL_?x2ptVl7CHe7%wCZEi-z*jfD&q_F^I)ewAE&jh3We5cPO#m~nEcDD{BEC=)bcvH{w^CC^IdOcQ7Ew3m}Nz@g5D_WpF7~uSLd19=NsM& zBfbyF=ARv237v{RZ^kkjFC&L>D_qgZ0Z`SpQfz>~uhW5CoGh^M8fcDwHc8$FaO%O( z+ZxP=L_OlvscBcAI>hcSHG@kn^&|Spfzxdh6b*ku1r%Z^n;(+)%U?OdJ%}!bcVQLj zQHtEyX~aU1@4sDTC{|!b;A*q$g?8&wHoIWH;bC#Yjfj7PLG%(F5iFed;?Y)c(`?GO z3p_-WEQWtQAvZPP__{MxI_Dg=m2fqsD;nY?8E*s5-}3{y$7_AKBdduKYnT$+jM*1F zLi}_4-U=ocy& zSbD5~ph0d!EI?-@XxBq~_TNv^qhrBw!HN&I?`Xp$9t3`3t-B^43^Ay|DbmKe&y8kz zI07!(cKq3gN=sw|J3=Cu7Zz^^4kG+*DhRqkIInhZsFtoOaK=CHr_&Srvc>1q#r!z< zYWO(7#Z&lb{d{Q8>WL4(x}@eIW)vp_Psmcu$6?MA=DrigyDH&jYGWqfuCdgpzdMZ}OMR zV*15)9PqDrKv(jp`wMc)cwsWCKTdvLS^P5gGa3}A*iFIsj1_VZo=?6A_QM14tK3uU z6QdXkSn3=abQ%jJ5KIA&*N29b{WF%+$bo2KY{9KRPF!qHYLT#qUAy|x^D_zn(BXVt z*Z%Dxwp`Fv5L%Ny?luxXDUt!*R_GnUG@xd)jrfl}(XHhxM1}OBu-_*T`izf9OMu6A zg#!}twFqwXy`FZ%#yYl}gkUUaCV9-wWkCw}gmp|^0XKtB34T{XK^B0zp3@LIlB^}% zYP^@NTLF*1^EzUln%jcs?_oCeWYgMvWpon2@C)L=w1QeR$VX|Hc2d7e#N}8pg2$m5@JXemOH_dh^wE%QI)3*pjZKV*vPQsw}fO;5huk4uy0B3l~#F74#WA7gnx1p_3%--E&*;Mu~$hz`7~{#H=~dSrBKr2 zc~X7h|8{cHY^h<;{6s+H!P1@r{XrosZB9fYhUfr|4hJKs9v|U;jR_aSJ?vyPzus49 zq|kX6T0D1%HnBh+g7BiNjt(r~>`#CG*UQfN*USEFt+9OptRbz0@Z3KkpWSj%2|_be zY90^>jc=EW2d_^Gf*v8!;5{_cg3kVF@ipP`LL0*Jum5obY_~EL@;j$kNYgRt?-(Gg zpk3(2KM{aD?+CERCpNgwyTic_!fXC@#rxD(vpAktg$EMO%n~E2J6#fLWt6Y=3uKsC7scYcPM^aRUsbkS~NcN-z>YrzJ=h zm7R?E*DG8F2iflZ463QXMe|?l`anIWdo2@xBSj(Xb@DRdAcAL|EMJg}i@ zxRT#<((4>`AMl2fg_NDk!odYq7_Q@9V-AI{o))5I@e|TYQWWCy)xsD5Yk2H ze2bF5|Cu`i;u3Q2dHe?@I@nrz#cf8J|EmH|x5p#?41eP$J0v zhkDrTqfb~VRKBjnzNZ%jslcH1Rzl?8*Gq$I57uiEItR^CCD?2*nE4IP3CwPx}iBkkUGtjfg{8Li*T#ffJ9{5 zg4ma}%67o@aS5*cg32uw;YPrvh?x?DnIR;~U(&v218t4=5fK}^5x(Hbykc?+YZ894 zjRh;m`p@kbH8=3*JKX)f>quOmE=4r~wh!k<7nTt@Oc@C+3d6-y1<1BvzmaW}FkiNn zZheoZ+6PyG$}b%7NkCd!r3}#OvRGA;_Qa-t9=VrGfSX0~kaiBrjd#7C z0P{1~Q+0~uV%qEj4jnWNIs&8&TJA7c=wcKVmszCvt^H5t`2c zVNWz(gv?X4zr$BIAksmS+nYlJusE-?1V5_MvrvPFN__IcFJec~6O7&Zl?>1t>v6Ub%ja{EL&wbd=7pkZx)yvr z&E6~IR2ky&J8WLDXI;VIWB}=Jgzb;PE6*D|=572`&!Z?g5v)Q$Egss{hnSzoVECb| z?owXV5F*H)sX=^GCq!Ynog|r*1ELJxPoEi7BlS9Fej;9~3zm@D>4{$Yn>{T(NW3TP znK-zA%!rOEg}>P1VsEfMR=*21BZq~X9iriuZMjX_e|FM)l zqY1qx6Z5l2wu|xFp}Jh{S9o_+>X}Y~8eZMD$>G30>E=w6)_wG1z~-@TE7639`mpJv1TJ^MgSLkDmwoKxsE0m0!w3;19AkS*sZ@FGPg!hQ7}TAt!E3` z{Um-B37YMRqj*D^kq$c!*5Hdj$Fk6?r}HeoL?yfOe2`471SW=mJ_#TOXmPsmns(?e z=J@u0uWWhXF^}=1P@W(ExGqEm{FiI54J2@P!qI^RhljC@@D}cO+{v_@mz|JM4%)lMJGjNP--Ukg z>yQn&18cJ9yqW-G3m-^<^JnVZ2MTBj=&-Vx|2TDC9U6;uOBjeLsQv3&5QxTb9TyWf zB8@aV1OfG=Zp1l*Isk?qv5N+IZJOtJUm9#07k#0PmBNoV3X%V~`PKCl+8j9gO7k|7ESQSyIZtMf4-!BfHCKpIJYBpF|JnT)bfA zRt84`pX~WFbg~90tbj5S{Vff=PuOM_(1^Gw8TaVKVxhf`ew|ryrb}r0QF8xBQBC-V zbsh`9i=0K?f;$}gL&n(mt@7O<)S@b~dI3Pj&y*bY@nK#f#`izonHFoe^?X9)8;JQ; zUYKCWB&Aj<&IL8D5A3E%P$*RoaRC4BFPHKVki*>WYX>=u?aD8A1WXqJ2RRJvvr&YV zOUP4@!-(oUHFNlwNQIfyisiH$mTLk=7tQ#uxm?6wbx^t%1f}6VErj7iBd~K-qcJEf zzJmN-4M;ZeV$)L*u8h%LLww-P$6EghuL=IyOS&?P*1HTP1sJz6a^lR*rj*mB;B-B> zVJW{fFupp8e&jq6;ECKIRU(ci(Ccp39Q8a4@p}|tf!3k@XgT8{-L}tPhw99nAgG*O z&hm$CH6LrLQF(&$KXn(YK42ly5{1;-?U5fhtuv-XPM;;*_iVbL_bYOmVV9PD=8%_p z3o=vgdn()3x{mgRo;@F#be*XSPR~pupqbl?`+oYyVS0%_bxXU#c1HGb^HM&`(Y@Xx z4zJ97Nq>*U3CrUPZ7?KpXkl9$q7=twbg#N8IY7%cRM?J-~h2% zo`P^D;4;Jvj=_`>V&HFUCs@X5P4;;#KBfRN6}A=FDNI`Q$({tPoExS9dQjyL_&S8@ z6mEeY6dm`iQjmN_L++xpO#%FJP{aJ=20MwpGWTj-ZpK(WnAG4ihGQ?yQ_yJ0p2Gy3 zx7ws_+4|N1>ojl^n7%@Wu7qeEO7g}Uc`soaRrElUaL^Rvbw-#A(PMLi%s#V zp}>>EQHwy7AW#LLuJ*h$8@Z)V&zB$P_l06^B6Jdsq}q?KH3vMoi4c%Hq9VP!#I>)? zrP;h(#RkwS*W@wZ#s`B%fEfpzFzeI>TfnFy<&T`tX{!0CxGGe9`$<>(wOTs9h%bGW{P$8*ohSa_yGHv8E*);fZd-$>?U@NRx0Suh)pt9@&{!YuW%9;GC zcV~S}ECs(DS z1{d=`D!X>b0hkyzrF8fCjo|Nj0viL6!80D|GYiO;@0T_|>PNvSpW*%uO4tl)GI$QN zQE97W+GqF9@<3vYV1t{sbGXSE8Dwd?&BQwjrBdt2Q7awdEodrsYU!(mI{l%QtvE4 z96sV8@q#w}QAJpFEQ$FoJHKt>@ntNN&%R^0i8a${WwVg%*_?h{(TV%G^(SCy?R@I$ zFo{}aB3XZf5RtJeB>+*K{Qv&gpraHxm$KY2+!v;x(r1BUMQ99kKtv&&w69X^J zy3_z%76CkthW*rT0x=Rlo<|^TFPXQd*9jI(bc*SweC@xM2>|{dzSE3(Zy5o5a{_ns z>hH&o4tt#I3`)GHGKv~_)>#p1{_s%)zLD*Ku6H!U8?$#Qx@S^c4t^jeK(W@HROWkQ$=P+p7KYz-&0eArowT?r$Pns4-vPz8W z5|b5`H4yn4@`2xA^nXk1{v$gCq1DTDwb`hd73E| zXup#tTa6(P)C70_UNRd)o9!-5TF$&Nw=bMEAe?AxY~J<`%&m8kZLP-wD4;^u_LxcS z%G?ek%&)K)l9@Y|I&lqiyCAZyVcP}ew(>RQ$>zdq)x+Jq-B6az;N{VtXs2fXUPtHt z*ekH2=Bytl3T^tpr+lCT0N)+ufZ10O-z*=(k%!vhl>Pg=NRUaiOCrQ)hRm}tf!&b` zvIIwFB84;2fBW)Xki>%nSn6YY=81CO@DXdImTm7@Sn}I#z|vW_M!AGC2R_S3oG}ZH zJ_YbAO;KrEvN_Di<7(I7c)1hf^qoRsMf6rkKgK^^8=lTuGN+#l5OVC{>YoT$VS4#layCN5B92neBd$IC|K!pPS-0XT(SU3FeqosBl*rsOiNnak+O}bh zPQpr!qj^5FK1Un7OWRQu0`($m-~c$YEd(oe3D8pP{Yno)vzx}PeISFj7T!Lggeal_ z{WAB33)%qNgh72^_^{Hh>gH5p@)it=ijBLj73pH9o*cF;J?g5PKBYi)fL-&_ll>0d z)9P*`!2z&vGj031={-{%?DKFA)9kFCQ!7huX_f0@f6cLRWt52Fw37cd==_u)%(##C z9+0AN4|BkXsg4>%a&_mELPPym37qh^9~7z%W_*wNk&%%8)0UY4kDV2*lGdRWri-I? zT3OLW8LZduj!TtYByt}znawX$Fn!~`aiz~_U##tbISQJm{O2AzqvBX~) zP#L=|jlH$C7U@qPkwB^0|6t|fgd*YbT|!_o-fUbho|-c2ySxc@QdlZ&U!}WqOsaD0=&MNDOL z#bT;>J!zWx$}i?A@r2`t_&xahe(yvZQX%)R%tyuPQjC6)=}bTz7rbMsg5>m;_Wq7= z(c6%bv&i>0Z3G7uNU0H%M*$Mm%3nxYybaqT%Ah6Qmjg&X9dQMz2P^_+H@Ln^bu#1O}dxklk?sDODg-bo}PLuwiSD>E2I9;YkkN1ZU_j@8bk4%Wj1L#c~9a$|@;{#3pK7{Jv@ATUMyOz4ZfNah@)xWEAJKC1=IQ_v!+ z6;}y_9le<*WEuGd~QF1dNnmat4sup?9S+FIKeL=v zjIv1?S)+73BTvxJLw$mqhW2g<=Q**rY}6%8hs@?(-Z6z)|Jl+bB%dn*noM-yPYW-4 zk|jrHl!^R)!wCJiRBe<^A$_T__{)vsU9K}_&bM0g)0KhY+^?em4yg;#@2X!i0LGE7 zMN4I^7c@c3CDH~lx3d5OE{kXiYc|TR1{eN$2B(y;+>wDoAW=Zf zt{E+6mu(%#fnZIQ>*fOKkM@Mx_A}lvM{QtP?OOVpA;2jTjE~wn=b8)WukVjgs3}S(Q=TRtmGVIv50f7OHESs%G z(FX_X##kq4bL6suC8kS_x9AeW|8P1X_hrx~R&f~~(;M<;J5fTR$+8Ja8PU`Q{1hBk zj`FE{F9C2)%58@*SGI-ldv5q$`017k@|iu8&CU0`w|T!$_uPuyG~8s&{1B>34Pibz z;%?Bv6;XBIfEfIHJj*cqBc3@`pX2amFreevGz^m}{(p^Uf%u?z0TjM~v*}+Vm|6j( zV}fGub{}C%GfjX~C!GLd@|aJzDcEjhx(yKD1DN=xTkl4j9<>%I++ z6cWxJne$%k43%xQZF>dX%|L!c5O#PmTI{e`<~?hwI-M@ zen(q$&@QcDNUY+AQ+4TssF}Rode?`#{``)x}4V9*U%!Z&C4d(jVWFHQ^e4bilb6Uv23b$VU-7fGx2|X0Ph-PBEM6 z-}C+di>`zmP%E7$TOg+dVxPpn+q%)L2=wssuW6wnylx|dp_r_+#kNmTez;BAYM!^f z(<=kmWFOBp?!{Go4O*RT!F5m@#NXMPsF{^nZhP_EET9WJCOJ4aI07>Vxa|zrP%UTd zC#<~@1PsQA3u6|%mV(lA?cRf}%f9Jqr8;1$P-GFX*l_2&?bmziuj*_sZO?j`QcC%% zwx_&eT5tzW@U4fNl=*vtqKtr}fIlp~wWOaT#iBak&AD`@|fBlVtX17;2(^<31;nF7lp=Yz4F_6_FUiFLtJ zqr>Y3kRIH~_z{o^lAz`REYY~iCGEnP8Bf+>&*$)quE&u&aF&{w<-ygi4?pC{9=4c0USZ233J6SatWN@};oxJdhrsQJjENSqac8gr@Rdl98rp#W;BvR% zASoJbjGu*}5eC%=tGl%Zq!3I#0_3tu!!FQ9TN_#(z+$PrKB%1~l8Tci#-!SDMdXHc z=9lU>*n)kE|Iyk_#wdj0{BG?+0@$MdOKTTD;vV&X%?Ik4^Fi^A9?_14{?505zfB*{ z(_g>eW=DivX>f{$#K{;5^&Ii5s+MixlTv^`f8fpl@aNg5*kiLO_1@P2saLd%1#bLd zamYyX$r{VXZ4VJQ%s)R8FR|2vyXAM-)_hWV%4=l}2|BHj@6Sq?^nS`$6R=j+ieDZ< zLqa2rTr%9W zH|0UY8o9ML9xCno)TJEbJ8x*1ChF2(k8tPpUbx+xLorWpwVM`*cOqnS6N;u~O&5^EywpN&Qg+7(QhBv3w*+>yOwo~(FyVVgy)8JKMSu=M;RU(O^q)k>~+AB z<-2Am{b5uGRO0sPZ#!oiWgrh>zrp7SBs|Y>z`#j%rzml*DTIy8Pj1jCTMPDw9)~Pq0&4q-kEdp;0tq1TA)v4xYdh z3WG%B#&IIZT<=0P&|szoJk=hYKPS)VzC9QYft#XZ!f}b;FFiO=|1@dtB?Fk#msxs% z3@n1*<7?2UYwCNi%K^YA@=>?)JLqjG;U;>tE3d5z+@G*sgb!-`9_>E4zxPp(tZE*SWNS|j z{3dRO|9DOE_9q^?|Ei*})DxAm%C^e|Vu#Xydn6LX*tF5wSSg%*#=;7#>a6DQoO9(A zt_}IQoT}k2k)a+&NbT=wFaDAGTl}4`{jNS~YQUDSjz&t#19 zLHXPqF!Y{gaj|Q8D?a`D5nvH+{fR|@K5!)Xvs3a{3>H z_=9J4mRAknEE^v3X{LbVB6r-m@}vlWF<(CbCFBh7Eq=V?MvMXjhHPwto$qC+3edSa zWs8)J4<_)0!Jbp8Lo#!)huIO{qNhWnZ?7~)g|N@Fs4FETSkH9< z=q|^B$}ii&t=1q)JTD;SF_5lF9^;9oGZQ8AHmI)C}5r$1)#gV z4mvOIqn8uFp8=N?Az+A7CrM5#13j=khlps1a=h4ftcABk{?JwG5CQeJ-)DUPV3+gU zq7GV1w}T1rEJq^HU4S(bh*DSTE;Z}>Mc94*w7X)5=%r)OMIIO}8P?j#EOJb#<)5w4 zCAdHCTB9z@tv!j|%n>Ki7f7pg$~lR&W+gNxnf8@+c3kzc3N@IxGHOfNt?OM~&KKET zBmMYr*o%#y^K>A8^C#s{+Cg99`OCJQdm#ekaZx{uPFT$M)@B!q1j!<8bY96UZmwil zSFAH9a>PSndd@Cmz;a5e?*92Q@T>`4sl+F1TOrUia6TUZJEP}u$&xBNrQWd7MMmNk z5M$bPEQfjjgOSc8m?GTOMc(2U{zv=#A##~Bs}IQlWpY~6Wgo&GOmJOz>vb}OS+^s% z)rJqo7o@#Z@5avMe{a_%xLcJwBfhk?t~(!%vL`EcKr`UJrNBw;BAz>RIt5s!K1 zZ=tkr45ur|CDO;S-mFMR^AUV)Zx5F??-;+oeNXiEg|reHG#!JU5>xR15Q|}>5TN={ zI!k7K$c<+URre*+d;7Qk1}mx-zC(&=!P%0_M^Y*y86P0+KER-U!ejPx!t<>L_f;n( zd$0<$Z;vLm(k9;*AVYjGr7)Z5D7mXV7-gf?rJ6nVmeT7quxrW!2}yLG6mQhU_b5&T zmn^flmSEKQb^M!OVdrR|a-(V6^Y&k!3IQ(_o9SH+#&<6Qq6#3pHOwYKd73&Us<;JS zqIdU{&x3*d;YI?noT0=&k3=N_=mE%an!soI0Lke?24Be%l=CQZMR}~p>Gtz4K|JXbk>$BRy!L#Os9t2kqyG5;`pgp5A~a>fHx z3^`HrJuGveW^GXjO%|u+$K(Yn>)+a~YuC&5ZcGCulNtU~PSP!nt3u-cM=trVdbM0O zt0!JGikv7GMcl1#CzTtbl_g*S*lgC3D_WV?y3chkd0~c%4JI_x83a9UG9~=mKDHSo}a%peuXyBS9&?o_`1zEDcZXN;t&+f_oQLoFNMlzr65fL?`zoZPm3oHB+2;io=B&vUb#G=p`$kaM3u283LyGVwKnpVD#@`GeHq3 zaT;p5dD>I6G9hYC!)-ze0>yK`lDn#^eVV^)SWQcZn@8|A0 z)(M!(f9Si4Gc`%VE(~chP8|e51f7J52>u7qa1UVkTRR-K479_I|1MvYhZF<(Vn%%+ zkT1TG{!jU$AR^`ts6d+NvY-7conW93!hHUA58OqSXnYPsiPpZ+}oI<70vH!^Y?wSdFuUWQ`~Z% z?LeC{F}>>uc~Nuj#5>COo1q>eXLu1>?k_Zs$-Ml5k1qzD*fS7m>s9~0P-XIv%3}y;nD$$FFwU56 zk0q=+>s2iU;+~PW<<-syfTA1jcz^Jb?>5(=w8ASDQ=(o&U7|Uu{=fs6OdF{TD1K(Z z_Dibb5ley;pnC}zZG1G}n~3iTJ~;od$=X)HQFyz_ zjJ#T3a#7J?K;a*kZd;vzrq4!aDd4w3gNa9qj`!vBk(9>!XH?S^chHxwM3=2O@>+#N zcih^FC7>8Gt5xS>iIh;bPB=3Zs8xw`2NrObOx;X|zb9sNUeC97>$g*0_xtNb{4H@s>u-Dl z7*JxMhr9y{|IG&{&*y8_P&?MBf`g8bSjIDgh>U7lkE3_L$fkKQ`8{z@K0{72WgK2E zt-?1i81V^O{H}JxajM2|tY~+m>=~u5n0oarPEG1Y~u*a&v0!%7edTQp7Mk&bTkEH(vwrKnO z6n54UwuCet5pt}2b72A<1R=HaKA7&S8GA*igzI9B=q^(icdsrjKxBD!?8bfj2XT?~ z`^2zCx>5Z--UMFF&fTfvPgU**p=(XzQwlV=lZXdMbQ2gWdY!to*0z< zmFPKD9&*M-_rEb0ZC95T0P2|3|Hqc`H_&vilO4qsaL2Y1P`uRq`RR#QA~&O zC!geBCDgo7u_Yniz5R(2wg~X0UzMz{p5wLB`u;^}>{5bg&?lbsNe^TqEl#@@u5LqX znTyqxP9T|K29)%mua^!oDi&Sj+er|hr}fvmM&}%n+E4~k4{ftNjGS=Ht!+{;?n)&K z8L~=JTL2m!;=h?32sDaZ?9K%Vk2dOd3Tl}NfIf<%w&g;xqhi9vFVKVoKz(R!U@2ASF?5bbaNQ!2?FJ9q;?gHS~22Jd#5?HA$yLD>A0mX0vRJ6tD zgktYLbWf$l5v*GvmjDs)_M1Bh1kwL^#?;D43qnhei;C0ExB15UY*$i_=eFCKe;#HC zau{5nubw981T@l6DKKq)P61Tul#xN^RA<%aqVlGz%PzZo4sh8A&unoYKa6}9o zV5eMiHUAxlN`{FT%Q=k9x6;xX2Z@k*2Yg~jkf=;MM5rJMX};d7`=a?H-R5bNJ95)I zsm%2O@4n^aB7_;6OL29^DlD(|SGN7AH&44vL1q@N;a4zDa$_zyrWqTR77&V+9$}gq zyLo;ZYLmpsj`{}d2?o7;eD5EEj0Wm79$n<48&4Ri{!k~)4Y9r_&%^S_UaGSvJvV)S zH8fK^Z!`IJbyvrY0@BK`Ou3>PRVp$#}Cj`Len;`gUZ*M*~>>*K;>z@|`(2HLRm zcTsi(dgT!2`?hLWwx6$l3otSdFtyGSf2|dp~#_1T4F@BGkxp>301uD zRwJbsz!}8HKq0vQa2eDQnq@`6clOT|jv^K_G$GS-IdbuM&CpS1iTQwTR8h?o1Un9w zo9qt^c^F+=} zJmTrE8n{DG6_mmJ_a7w#0o6CL3qczNXac_{nN|IOMMe(Eub~o9r)s<2g5>P^1hF8=(0HN6-p@r`2=udK-@k7#4XwXLfq2+ zcjDISe-O7A(9(A+Usoz}wZ^Ezw?{MUO$D3=V4iS~S@vIvDg-3&^n2NM* zj{_%e5R?TfKYfohGa+=`Fg&9H*taME?=_^PLHq%C*|E!q%WA-S^F2mR)h!@8XB+aY zj+lIQdC&FwR%K`3zIpv9ZVKV?SnEcQO-k2JB;90EA`LKB-_mIKi@?wPp9Frad&R`L zu&C=Hr%?-G=pBI;+qL_@LGbEcxXVZ&OYw2KGM$NhjNwoqAO^^u0pSq?;tB<4vVk(a zJ~J`{Zdy#V@kU@RvHv^$O?ZmtCPRztfR|ojtp!!a5`cc5<2#Z9^fNz57~_{#*%HEY z$N-k;R6~vASS$A#G6ksGoVkThqOBMT|`u5BD&i5!&>0 zC$zvUoiCO0vkfTlu>Wmi*=}{6!`k9^oXH7q^zPng%e24;5s_68(pt1Kb|@1LB?88T z2#WuM(zoE)iV@tHCzLP3=JL_*4D*MW+9BfjZuthj$wr>V65O7{x|YPP`+TSCHRZ;w zbLLl|pUFp54M$PoCY9q)R{Nm*6&cig@c|r6fHuLRTsX=JMU#dy{2#^w+hFQdWcWaR zvxO27li+dos8h)aBzxGHM-`Obc_v<#PbA-vZHZa?M9?ldXK{fja;06Q}CH ze>yT4r4Xa`KoEwxQ=vr|1heRCeO&B9E%p8uW6g6Gq&?k89Q110lYtsH*yxs0_`kOVDwS&8y zCN#Spa-I{t#|-%UVz7uLUY!fw1=hLAHX_IK$g$FOJtcAbbDR|&;Oeh(`zqWI(CT3P zf$|B42xIzb^T+t!bt?7K7Kxd=7LCothWKDBaj}K$pIy^Lvy**BqUwt-IOzi<1;q2s zT9)H`5&!-s7;btjHnm^93Zo#2s_$q|Vo|Pv4@xm&qwzAI*YoDq$C1H3P*<-4E>pkC z+7k{e`1O~5Vxu6~e#%M+7q@?P$FN(#YFEd5vhg8+5~2Uxw}E;#dP4+0+)2h2AdQk{ zK>lTxF?1Vd$!r%hzMjSzAwfh!^`3$P@XRtKh{$M5Csu4OG^4TBe?aZC{hE!1Pe>2} z0~2AtNU!E^dyn5-l^Cb&hGllw>$U*wSs|e;6XwZj_f`+s>o7 zBDg0zOf#fn7<9{lSsZ$By-Z8&&22e_2jC~Xg~t1073=%deIJid7!jc^d=%`V1Mn_2 zvO+$t8g&q;_E^<7njHV=<^AuY2vRci{2U3{AEaZB$?~mGDJf_pGc%V+%{H#o7)%BL z@488A<*(kD0N~xZt(XH`ga4R{44K0P-(5YMQLmueD0*RX4Wwd+?@=^8>IReN+18(k z9{|LGu3Q2nNBEWu8*u{Y)ohY_s(Xg&TF>cf6W$kD|xQ!PPwgQWguJ)P_pt27QUQy zS}wqyRW-r5#1Q7^2m8Gueia+Se!IoE9LSRyBCYD*JQ(eO;r(Q*|NUA&L#nQEq2h#G zA2(+W&M0<@m@p4>5`$ymRy~O{E^aGfBVdN8#DH;0mQtDF>noQ1@4crs|Jt9u2Wt*J zRv9^D`MF^g`g@Kx|Yciea{$51gKVCS9^Jzm@yoCZ)Hq)uTCinC1 z3%=hk`r~G%_Av;Vh$C1XuO0oBqt-r&@0SSuz2|#EFZOGk#YCnJRA{bV?>AVr)nXwK z7xM1~GbgK;ynr$t+z>2G-IO|9&KI?`A#fk*5(QR1ym62#2LblHi~}%-@$0mxbf}mG zP1UdsS`oceHMwIy4PbfgJ~(0;j!j5+%+y2~ZV8r^o}9S|)OsjSAGn0l?5BI|a9>-~ zwFxb4<0OQN?T-Y|{s}+0_df75&M_Y|QHUor#18Ri1rmh4Bc84Ut#j<2gp{zaKAT@) z*GJgNI8a;3ufj^|qYcTDA`r3wvA0R)VTSx!eivKC2lA-$Qq)#=01JEJD3y*cq*L*L!dq!YA-UV6dOHLF^sj#sO z=YhO_0Jmv$6^YWe_@oZP0}5L8uE~ZoZNkI1v(s_I)}~P zuliM`RXJ>~KR*e1&hiaiNL=ch#|s-?lpRiy*hh(H@o6E~8IugX=NNaRXLUfUe)r?V zM_6^C&-?em2#>QA+pNp^rAUh zZ<-FKD8Ui{l3`L4sQ7JI*YQO_yKaom=H zy(f4d&IahqWr-iM`0o}$PO14#uPmf$DgCtX{LN4dL-%i6Lf8(;B5hJgqbGL|)5i65 z38pcB>qJK(6=0C^VkX1hkukbN^v)>>k0NR>`5jaA6XX=tPWG|8SJiol?ppDJvUv-L z?(*=%Do`jQp+s_1J-6h4QcTcLdIc0EKy1cHC-&ZjWk5-*%+-|8Lc~^f_(+exE}3qV zOTWLqEqqSq6?ZZ8+nelnEE&j`2egrP&W2a9QM+aDoRJ9R1=e=C`8{Yl|&^MEP8oV0C)oLwfhBoEH4Zt^ob_|d+BZA9>cS9 z9^W*GS}NrdU1o2$4W8#r#5KAl|8q>yJ3tIt@16ekvh{NN(rrC>(XqmNf4{Eh1Jw6q z$sdp{E34go?GN8yzoB?*{|*Y;`+0As$m3@im65mk+nl z&X%pK&P@SZBY|9qC%JOl>zT>_2B8zIuJSy!3AxTQ|0gAH2xx{Eawt{;Tz~Wv=2!eW zJt@26G49pceK?@uxM??$8Y=prNHA+t+w{|ALdHW*qU#<@bC2j8RvZGn0xxE_!fi6k zr~5Uf%zPVK^H#oI18D@`bVz=2+XM^R?H~xr^_~LPzM`%)7COhuHPv%UVSwN?eZOU1 zf&GY3%w-B+U=I!jTJJPKt!^NtW_RbgYQ@S}P$ z!g1T@oaaSePBxv^a={>1F+}fho!hC+)PAvZ&|67Bj3}05g;|YR-$|Rfrn>Q%=3y}H zwWg77)2`UblGTzEzT#UO)vF;={)Bt)2{OGtH*jZd0?TY5(0$1Hj`zshySHr97X*0u zejicUxi||_+Y3?_JBFX@dG8#R)ed-#3)`%|-P0b-H_S2QcO3oLUTgo!t8uq`lgq8+ z4$jFM1f&rdxGqHQ^~+E?>eYGCa0+x==Qr&WUl*|&?cXeS7W3|i={+M$jeJr6`ndS9 zqvL76l&M&TQv4Z6r`qol-rV|N)|c+NC_F{HdEYOQ{$zjEy-!i#2V+yTp;vE?W_Lz{ zTj1GRf7Vigky|5Y-C3b#zQLYsdwY9ig^bnF)=ad%(@5`gSxo%vO^K@@J||CHyPJPF z->@#ux-x*yJn`&wD%E)|HGCfg!s?-N9^1Uy0WltYo+rHLUBx>Q%*sFWO_#Kfms>(; z8kLvP5`cLj&3W0&PG-fEBWjRzSZE@D%va^SS-DZUx|gq3wO%^L%KGD2+fP%BO*`(e zgpv^CAS^v_{V?E#ZRp9DTXJpYmYLZ-sM7&Z+$#3A%Yan1M^)Ze!VSk};f?C`tzIEF zE$EO7bl?dy3j;X9Z=9Ra*lu=oX$2eCu(z%d6vRpdWac>3OQ^xDc z$Xi}yw^2>8fikm1_rV{};TDY2%vML=7ll*Ni=yZeU?|Pr;NtUwe7aM3aVOg)1G8&M_-vhZ`T8zm>nf-E|=C^w(LCt=3kG>6>)X-d&*(ej*BUL8ym9Yzqbpr z?Q)&c7bim?Kg%CukHC-4cWzMjJ+J`JqL6SL*016hIn)v$LX6N}bGZaI^;dy9yTdkV zt?K>vaBPPw57$`H1uZWx=BW1)YmYvJ3^>LWLpI0iWH{G)i`a%v*kK;iavEi;`CzJN zwGdKr4ZVD-@vtwNXJdlcEl{V{?SeeMZm398AfZhT$yvQMRveb5Idx()7p1WmmaBEY z;1dm<+Y!_)(fNY^v2r5Y5c|sbTX6Jvn@}KBpA{PFfcZ0J_Yd9$LK?C1jcM!HuWZih zXO)NEAAWooG>1NI+Jt%?g^Te^NfR4&R(=7hkI7ZPjTnC&uoYaZ`R}sWZl#LM1fE&zslu_ z82?G)wy;eqyKjL(a6`g-yD_w7)x+>x@@t1f5WU(Dv)!4;{=ngedEyE}tiVUCR5PCJ zK<`Ajg_1;Dju$^!k~%NQwWJ7`(s!O}=0wPufizG1g-m7EM!TKV;NsegH(r9@s~2TK zZjbi0nnmEk{)LVN$RdkDxXz7|+FFB8ab|mq9FU8puN#W=n?T6@}aXm#05AmXz6cS}t*t7-i_SQYH94!v_3c85$%0!R(sFi=e z=8?vL6ANnZ*WT(L?EIqm@l_;`2G6S(=dfVCU4{AH>iilPL7@KKoqt3NEXQVQ@Zzi< zy2$CoN+K(G8B=ofFo0M@KPu~i`RVy((INAbr;(W3xc&X+&D0bX74=`P>0!yp*vwBUnc=Rl zj)=OTyaRl}!i4EH4o*fpI*Ulor~B33KI z(})qzzW2yhI&Nm?iGF*vPjdaNXtCNT?&GuOmFD8f;%o}+g3_rzW>kv{^c~$@as-ad z8d*ht5kv~%bX5lMGh@Qm)}kQ+TabOC{`(TTRO_(Mx=@ZR9TUUAL+2S8BLdWG96QYDqFRRmyG|%g2lS9Qb2eqJH z)5%ct&R_xkvV6PfTQ+L{pZBZRR9PE( zf9tb=X%jBjTUIiJo3X0^O>_S>HnWc~(;zmeJKFhpznINBProd7^7L0V{!04W+!zA|R_I0DBlH+r z|FO+VM~fv5KK_CBIH6hZ<1)^Zz2(%jvO?8ZtH~GE=(OCx`Qlv2jWelf#ddqlrh9=6 zn~{G;62X{vu*EsyLu=n2CE}>2qM^Y;r#8j>1rnie0Zm^g8s)ru8@0!l{G$VslbzF~ zR=rzVc@2ka+t3HnC%^Ej%Ba!J&6vc@Bm<{1?wqs|t>zpJGj`nudt=}+a)3>fumL^h z;zojsfEB1FkMy_ABJxq4jF5A$g6;aJ_8{dw2rC7#Bcw^Fg%~&meQs8iXEJz3# z2pGtOK5U;j9Cw|zy=%xbogmCZf*2^wlBJBYtoap7*K^L2SiSYrrW_Leah>XD%afmG zziN(Bw>1CTq^@eI=X4`IdzQ@EQ;+R8ijbO9h-=!TKKQ4Nt1@c9&gSSY=qg<|Di%5P z_iI_Dz#ARetbJ%F$*$4hv}W)%+ZH?}CbDTWo{MKezZ$-5W=Ar}5|+T&yY16s*&Y{& z9v?5(mCJ~$B*tYL=LCuOb>``F=G#m*zNeI9MJf8TntkzLt#Md)l9}@flxj71!|J7J zIf-e2=z6H(Qyi#FjT?!A+P@42S3 z-8T6g)4{C|=7FQ+!*OTy(JOgXBSILRxNA&t6dl~4$jQX^C|Uw~aVGDn@-pG6h8N@V z`ee;1I3PdbHA0P=+tgaW>)X?##=VYh7&(^mb<0XmovRW>T=i(6wIC5<+0+(I@gwAN zy{>kxudJJ=&ebo#|5va;x@R=)aX)sgYCz{=Vq(hTET&JJ%leEV)3@;u_}Lx12T%Y= z=`1C`#N!nZ2%>%n`#PQ-hmB697i}ELex0C2De(eBlH9SY=4gkA;y4bo^Jdm{=mW#OM5>H;|$+#EvR1264J=98Zyv*_2KE2$MrDcONhMzC2#A z67LiLWYRHAoLz$fP7_La`EK*nH?JIuzHw^Y1#gelk{8_DNy$kr@<+OX204Qqs=W_r zHV5j$(MG)=h=f;xDF3FV-riT8p|oZj1IJ7Z2jUn>SRKvosrdiXl(yXb7!vJ(gH(Q59ZEDQf6V4&!YoCPCT{7 znF$$_oR5$pek*@Tx(=L>1eyGh%lKSFZ{|9^Mv~Iq_gL>>&fksGsuRwW)Cw(DE<45yFo)m(=0!#viE zQ_t~i(;#$0I)t-7DD8~P^Tz$K9dUR^aw|X0J_jxh)l6Ng!lN(6(JUi6RVp6RF2CKg z_ZV3DviJUUH9G>gz5m;Jm4E|b#FqJ2L9CFyD=g~3Myvz zc)XvQLu(02eSP)g!p}tbwP?CKgDm%bfRZ;6TG~5KOB$jF&JRAmFpMy~hzfnX<^?)0 z!O&G72BPWiv>$%5ZUK%vTsCU<47r-tQf>-$k(fTcE{%j}x*f@LOego_!QqJ&{4tai z41l7nJJiGEZNUEi!Xkha!BOVchlyKLb#*eBBG(gjp#%sQ zEU@mMOhA`^-bk02!Pi~4hj%_+6kYzH30ZX--uhvKR;biwm~>Wc@SctO&raGM0_FEN z9&FDIXH_aw)a0Z@lOy)gDRwe#Dt_?fE0c3pIVZ+NLV|>=lzpHhz|MBc7}n9R*~@{7 zH3q%vL+aI_B`u(@>6w5L8vC@Stx*N65X zH5X+VDo7DtMCIApI}Ny;>N@iCO33v&X<&HY_e`Cu0+JIK4$C!;%m;QkMg`JNFiS(D z7UKdJUEvJDeGODWTUdSs2$>W0g}dy>ni$vZjDI#)DZ6Z07YK(4FFPWFjNY|4ob~%O z%Zt;KZW!nJ4KM7p8z(-#javLB{~$a4QGVSEZx%j9^Hj`*0CflN{ZFa$Fm|y{bvE%% zW2Maxz(RA{HW-KShZiFUb7o`Rua#RIN+LT;zI#VtdmGjNDC@j5QRW{=ANYk$ER8Y= zu63>~Dq34?cz6vF$V=w`_(xedBuK9~`W3P9Etxy0n>7?xb?~C8Qu1l_1Nm~})j3!} zbRajE@Kvh#*iR1B>-=~KsXO)VS`JH|mKl0^`7`utd|$6JWf{`EGcIgt&-SZZcyp zOOGnjELk|_$EuPK?fn;-kw#VT+6}W*X;)Fbup5VbazA`3Agu|o_+-c&OHY@N8n!K4 zJj`kft`(2|l>pWX^jgatZh`yMnku%~=z=pWJ^9L?GR4IyKoVxzi|n+>dX^sPXV42y zC!(Wot$xxp7vvx|N)UbYLo-WH+$V@~9RC+^y7uiB7Jo$tSzmWh;2*r9Jgmp;pV4;I z8cuD_kYM{bGwh2>D+Hwt6!XpfGqRT#MKb4KKP+~@V<^mg_1vby27lz^D&B$mbz|ET zE8ayt>M~Ngg|hRuHZ*84E-hD#xA=TQ!iBh(@fH}=KYF!h{4uS}5SHQnSWnb$CFgsS zm%Rh_5-%(_uU*1K+Z)?fi4#iWJCyt`J62Try~k$*zcn>GmB9cFEPFpAcOM~ByF8wLPn#dHNrzd23^w3{c@wq{!^_V6_WIiN z*i{Dd2McIk?$k@eCo=0oY$)L%iuy6Ga)GnG%!`#?gG52Ue8Z9#r8;$%3kt(Y1;JB3lug^R-O-oHE{MrTu_AU6;Cm84w(HemWJ>P^iX= zB1TlQaK6^)1j55-owDj5TZ`Ca`hG-Oh;=C;LledUlo>!fFHlEaR3)g4y;c95FOl6% zQ{VdmHdjgFQV@b>%z0J$beL1>F&nC%sU`?<4VrF!5|MZif6@Y9$p^6Og-ij~T~r(R zA=VSYZORWQyC{xLiR&{@3oNrgAIZITe4f9|B%Ty)!54;M%I-*uN5CUPWZr#C>(kiC3B1Fx-_ zvf}plM!$)Y1|hZ&Rot>shVsNKDuR{xIwvAr?*nBZcx$LFpdmXb)8-3mqNME-dfV7)Ay^?G3Sl)tP zf*)%C-aAJ5Xw;D47v`d7t3|!JeoZJElho+N7N-$&>g^hcJE=IdG=^(m!xrrzT#Y;Z8IGClYdLK4^r#in0ism&@}W;0s1@&d zdj{ai8o*D2SgeUEB|*GvqKcs*D)Lld?*?DLIXblTweM`|%vE6BZBWz6LpjFE25XaI z)9(u7fL7k{`6I=-7cR^shkmVfLJFdz_TA&HLyJ~%8H0;TNbH)c(yrqOX*buyfcKCV z?O}G3$7W0X+npUvK58@L#&^-E+@5~^&FXKhgpLzCls~&tcJO((4LsNHS7J{E!gmK- z6W6%`U}h0kyB&PF-Lf~O)*L{Bup>Ik&Z*1Tzz#aOe%qZ_5QrN6+rRfpWtQqd2`F8) z*;PL7;tq|iqG-zgoPn3kw|`CeUBVxeJxpwx09M>Mej;gj&0ZS%=BE<{rw z8~Wa^#ixSDxHx=I;)pgZHSZ*0M6Fna&9fMr5V4cZqlWa{_nT)|UmH%TmFhN!uf+b` z&aYvu?OaN@pwIt4O~Qiu==kvcjfeLGyzzgQ3>n<5!w9fm$>eodLR5B-IpJ*LhKe|VIG_2=(Z?1_o?Z@;^Fud@RbbWmr8u?Nk0f2~R*Kt-gXRb>Nx;GY z)5-arc22`OqhOp|8JViQstp~Tz8)=8QmZ3AIgs+5#+UXe3q8i=Y&$vH(C*j|-$G;5 z;M=Q_ zv@5rcm&a{J;2;?~->UqcFIjk}+jTD3tk{)-@F0XZ)D`{mdK%rume2*CTvLHjXrCv) z^>~tXcK+kS(5PU@aRHPp0jLhj*E4PqsJP#;_DP;Oj6q}`1X4PT@4BiNP z`dJ5w?Z$hZwP>;9Y2~Tm<++b<6&ji>d5EP-3|%ggyp&x_f|LOff>bE{X+ym-YrUf6qaWJ=V` zK{MgJT-`L76!C0kme9I_&g$Nsfxblk;=(yXfRPjv#DG;%gD2GokRQ$@}t zjj*$gKbf5Cx9XHPXZC%+_C=&$(WA#`l^>CwKI5pJpQSYPFbLorBQ8s>oSMZ&~3Y^ZH{#> z;xL3(^znJN_vMA%(CfjesCf_#X52RGZ6E5>eqzKvpByi4g8PNQfuOCnNpdASg*bYwbs%YMt)Sjh zl_YC$i^!pke~Qc+QtQjW{08n2a$7U4HhtG0!vOtHAgu?NZ{wPHUxPLH$dfpChJjYY z&{Nv-!l8L!=xotMpF_LOb$Nsx#czK7&3P$q0gwU}HzUw7n`NJqvpmTVlxPxB;}M*Y zhh(Ps;ih*llTJ@C21PTI60@=qq_GsNI|1ga+L0Di0R*!P#*zisSl!lwx% zceN=xI!IkzkdKV6D;yZdvL(xcYuW$s#2eN$3_9)B2`ZU_Fr(fS8#1E^E_qOJo3eDM z|B_k~c1|ruxBIo7Gt;-Hkb;I?1A-=ELItj;p5#QHfH6?a1%`nvhT|h`c*PT5#QJZUwMG4oI9Ku`bs=dlKm;y6OnroSs@i+ zk(Jm6A9e5PNl@w{BZnzqgk$vg3=q$MQHWBpbE)5J!c(Fu=0=f)p?Dv?7QZ$k@|L9o z$&z?o^HI`wmwilmHTaX+XIwHQfu@h4PorNX`B8Jw2s>p)Q1W?r{*bP`=DLjOyGqa_ z2=CFOTA8bzdDev>xof%eaog>yo?Hx=kH$(PH3);rrpsjvjE>ERI2&E>wCQN~;!%)p+0)q`@CXUgO@5`vKnGZa2vYytSZX_an8-CxZ zq&D^c+^NQ0jG0i)3ptUHO{?L-7qD?E1${s9v3Q^e%@fyhP$RUvxvjTt7w(+(PJMl9 zG9%;Yp0iHgn%hXNpT21!aMV4?cFSUD6={(_HY1r9#Ow-ykS5jo@dIybOnywCyCSpj zW&h`dl%M&b9Qk*YJdcJDu*^h7S4(^nE1jDhxGn`P9l-%2*kPx=_|LwD_-uq)JH?yBqk@5?Clt4=MOT*`+4<3yi_z+&| zXy=J6uTk!#@kFvoPu}5}a9dS&%c)!UcCPEx)wF{$5ztPY9&ObRMsTjaYSZ0RNL9D@ z12-NYA_8>NWJuh*E_-*PWpK68L#i-R-K2@F>_3qI>ukj7XCWZs?(WAMT_%BiSekyl zY<~iGQQl`|L><4yJ+jC)8>B;Hf-+^V3N*xpmzP=P`|{bHVO9Oa+3HyCp2fiDcPr4G zwH2mv5@863ka8X2P0CG7RZET7NN&FpE;MSAM?8hN(T|J_jKUP&yDNGy>X(O+D1W+O z&gL^sf{dZnnJ?e0!-`9(JSh7Bsb+F*hF}&6YaVVj3`F*0Q#LV5w7SdKBD|f$ZN+czMb= zyC#4fFxRee6(xKuDqDu}^4f=_3c&)#03!!v-CLKcd5)A`myQ5-pZsG86fr3_w9Nq8MrZC;J;nD`NAI5 zDab-5+q`IkFlkImg+wrDBrgGX+=$gd-rl~d=r3Xzx$yGp7x>lJ0^G;)FO1%~N!&B4 zDok;z(Q$o>YkNG2{}{<;Ay5%cgLm@xbG`><8nzVBxOqmkg-O?a*1JC&=e54$n{(mm zIr)HJ3E4g6p9E_q-#1Pdq-i>MwuUoT6pcxoZH zo=qBlI)pV!edZ+%l*s|{LkF2kBlrmttwH9^(>r6>VEp8Jy4NfGS61dXxxw0^kXUQc zOMuH=W@3KR>XSfw6j&J(<$a_Hef`n?o}mOow`!p_qHp2G@V=njy;eMVsyuGgbTkSM z_CEacM>uN3kO)z}JUUBpTv5xYtAS_l&hmm%w%BuUB6g?{jHG?J-Y|Kzh6G|zh2 z7(K?Dltkre5kM+bM((-rkQ|mCI~D}8JssUQ4;Eoxvr+R-l}F?^%{Xzbxj*cJ59?i+ zx8Xtc7)(&EI6?iMa$v1TBM}I!**DPF z{?mInp!uqYQ$P?*#KcG>2<*4{ea597s7HOd2wmt=G(I`f5IO&(E@(r;OUmgRIJS!R z3FOm1WBJZZGzM&nK>=P4nS9?=vu?s>I^3!US}FSZgYUf^sjUC=dFo$ezlzst#1MGz zj;(+fk|6KoRB`OL+w;0;$Y0S#4V@mndu9NoV!7l9a#e6-|R-P zxWYd{+zHs>f?Yn2nn>#zz7J{?y_QMma!K;oU02zq3%d6x5cG1l@jAE%y~v4>fZ z<_$uegb3buaHEl6nB#G+F#>B>2x(7TGM6hpS7+a>Io3x?O(*!1aC+n{1*jdK@Q*z} z>Kh`XZea@rL1R^rQ{RivnM5x?aUHm);}-n5&5^P-J}Z0u>ibO#yVfS1{^mfQ>r@e( z&Bw2Ru0DL{y*Wz3!nSv)y^$)ChAi7R#)|u&?||Zez5_1Nsjd&egfWQiX#*R>rUpzw z8dOO&>ojk1*PnOApBCz0j`JM$#%f-2!OODhdhrXM)7uGC9f+pBg()lpW#i!gms#W2=tcDWmEG(%ZJ4_6{NMFqkJ!!YLf@k^`V2a30? zF&k@5=N}^17xDuiYc8v_|NhuJ@c;R-LyLx56L*2CfMpZ(oFWXVN&Cqp*>G)fFs;Gn z@e`ZN?#F8yIhg+AEk2^@K|$QnW~RuLvzO>uY$P4Fu|-dj*a-#P9?yl8VS*612dufT zk^+q}%ERCuS&=X6VWqAQdsEQ)RoI|a)c(@HVv@0>cxkB)NMf!(+G4&L7sq9FjjK~_ zE*q1K!so8ARtP<^kV4rU>Ib6^FwE4x7A+CJSY8V`Pa*H}R$@h!`Xn&IEE}+u*{NQr z0jK#zYTL&}jXopvluphP#-y z(Yu(KoDS+hIqvA=Y+_Ej&N~*!`I&Xvv`+et@eXg03=4#p)8L&Z7(K+sqw%LT<+`AH z7Bkvzs4?*Clh5jZd4MV~y&CT^p41a@;RAz{TbV4cUBC{2@bm=9rwWHm42psa})v05r&V}+Z&6s9$} z4tpN--Fb8;kvI()pg`~}VXwH5sJ(`d$blgUwXAT3zrus}U`Tw#^(qx;chJKjC+5DcbLk)~oQO#u6J!!bp6X2-@X9x|4az)JuUd2&d zAq^Un9kYSOF{V9$eT-g`s3CZeiAOX8&oO9%r%K&KF#P(tSVQDmTtD*qoWYe;PA0%Q zfLYQE`a|?EJaatQXzJ$TaLd^JdY$rgsi1Jv|M#bUi3k0V(mZC-5EmqYH~id%;eaZY zndLIOx$lrJ=HS^>#>+3}0i#&Ahx0DqPIRWi-*VZ}qn8x$mkdIy7t<1cBoixAQ#2@z z0hO$Y=~ zG!YEnZf2*!t`n>E8M2JTJw2uS184Zm*6NV-A>`pBFW={>2TSIJD10QI3R%nW9Sr9a z&Y!FS69(OhxOwcPPnx}|dUk7vAaW&h$1oi_}&S4i74L(0A!*0u$ zz7{W(FCAKQ;_%y-5OnlGuBAxIG6k$>_(iwNa!bSBC^k4qp`g9L? zC|vdMmQ7kcB<{HOEFqzc3J;#Oe%h>cDWsdmn+u`yo$ff(jc}9`tUYAGH+g{1 zN%DgMU8I4Q;cmdFh^z4$jeAdIC6_OA?=y7asb62>1>D= z#TX(L&$0^uBfzYws~FPV+F*+5-Y*XuHqw9pg=j0YWbpj+-+xiyPfw6#IT+?4(rEuh zPZs~n=$U#SmiP6_bI5qN)u4paK}QASG-`tfB`M`BGm1<$#4v{;d$txEsL`{ym4Nun zi|=@(1jHs?m?|(M90?)mD}_s6FraoPvjxrOKrrZV0Yz6dM+yq{-FVMHfn=_JD+wTO zslTt=Mu9J!jgWovh=^W6QyX>&kx>>X3(8J@Oj7g`zFTb2)ej3DQ~&K4^1QDE_oE$} zKx=c@f{i1H`~)vhrb7SptES>DgBEz3nonX9xv&TVVvv#gE;ABW4kYXNv@`oIe#z#n z{AR53}~)f9)M3vI<=js6EqBkz@+ED*JsU3!*!y@cI2hRyee52kia zQ$I`-0`u#tVx!Hp&t9MZ;$}^!c%Glx2;%zM5IJX56iA`Hs2A(2{i_{-9%Lu0(?x{< zw;oK0Rx(KmzFjS9kXVlQS5^^jJegn#XOB%uap%X~BpUKw*(C=QrddDH9Ec>#n1@F^ zyVYoIcTN!EDn^xz!QaT#`3-WGD zf&T^hIJS6wn4@U$ZKk+K!*Tx;dn7Z~SRCF!s2zW#nJReYC4wqJ31r9cM4cH!s z#ih}8_QBX{uSF1Z6!Sr!LNYjMhZOoW#qifUAyk7Uzx|;OPMjaDX4dN@u(ocH#}SM{ zD53d~9FdUa1bC|FR9QIE0aKhoXC#@O?>mV9KgUZq6TTm?*49ES+Mqz3Y_jpu2O>EC z-_aHll3$2HMGA*NU-HE7Z}H1des2mc3zE+s&hSHhU1$NnyFbEs;e90*qL)qvli*u~ zt;L2BFh%8x)SgipI_dabH+%uJkHMqq@wgsl>$f};xdf?vT=Od^I+u$;E(U@7e2D*+FlfQADS^->k{4sZNeFyyBrC2jif zt}nq0YILpz2Dxk5Z3Ma)+hLPFc=#SShALYk|BE`|HxPF?)D&_}(}v6y>;`^*B5v3o z!uJ;3I4(osk>fv>MRoF^D5>Upr6kxNl!&396m|$Q>1dl5w@}d_>1r4>)xve(7-I@G zGgNGDcLe-47N=eORzmf6cIU@Um)4FO%b?I$hm3{8nbm>enb4|4+3z#e3U=+vsErSm z6`I#Y2Yw|IW1TVzfl5x5fx{iFMB05r&Dk}QKpoB=p?R10Do>47C|Cj=axV|=tHK2x za(ijpx&GP6>ZFyurV660>aFwd9JDZ;9eCLgD-Y-&=Ow6RmlPhnzUV2K3T!F$sVH_) zt>O$$^a4qw8px8*&|72AJ>07C({7DOy46pTe9r3U0@aPB*1p+{#rZc}q!h{7`MgFLbHl4FS{;)l$X98s(MLkFe(X={E5Kq1e19!W zAVK6U?hs2JyFgITTw~UxuM3>SaN-_Thq^R}r=X;)9soxLO~z=ocL|1PLd@KgH41Tb zTe#;Y=8y++NHoHZtQgA{8k9(ytwyC&EHP8sh)Z+ALr2YN_0oxNp%pst!pvdRoJiDU z!x5q0Amo*daK%G~Ym_6pVLbdWe(UYjM8bDsZ!QDa5(r@4n^Oj(l`}4UhLx6g$RrlO zGk&D~U&6KjlB!&7rWEv6P{|uvlk+DLc9vS0_tTWiaH*vdrb@)l##0%%FA$^Ih~TI> zy7FtRzG$eMAW=0fB0Y+QQWyk^WF)kuIIx*(8A!gb3;JaQhIb*!1WJRwWE z(2_H=EXKj$o~+m#Shn?1l%Oo8T8 z9{X47-!UYDV-Utjz3`LcO+sd@#-SzuAr^|!G&*TfOag%3n$qZ>}TxK19(PF>oWy_oFZtO{#7jE1EM8;{q zz%i>T>KVMdrT)7OIe_5hL+8{0Y&-|fmK$Xcz8I$^@k`Mr{gy**u~8WvIWA5NZ5F28 zvam@s@KWsJJuW|{72X|xZk9Zj{^9fEc>|+h?X#5%X!AruT`IBb{A|?;!Bjcho6LhB z=gmb zwidj1VMQL864V$%3an3m2NP=EG$MBO7U}X z9eJjBv!Ur-(4|~H^(&Z{jgS<>ay^*&K)o!$OR zY1aGtf^~n`piUK&^BSc;9ZfUyA7v+QqS)*}kesbJ?~g$PU$b7JmQwqOQgFR@tNRl_ z^Gk=PE5qyKbt&(yIB~CdFH9!<&UATj8KO;uY)(K8pl>B2G-~73FT(BXFeIGwp#q*- zM!T^SJ06r9EY`im7j@qe0=4|~u<5+ESu^X#H3w)@(N5iLdmPhFs=E471oM}JpE=4= zRV%DXWq*l>Z#NArl*;EP6udZCsdc}voe!_C_;?sfd~R~_MTk}A)WUwDFbVnka?WZs zc%?J`d_zL*XZ%}%VEq1H5Y8bgdr-qXzWeAi)&$LIzEC;wygf3Q;-W%}YQg5P=Yg<*>vc3!7q+U~AG__VhFS1kUdP z3MjNhM~s3_IRNfOG+x$=RWv2V7bSNtoK;kHHnI;-|Czoqr{euCd>vk7s)3pN2hlC> zF{+X1`n>9P5($RzZ5%EcH=|9cdEJHg3-=A2r&eq!t){b*B<134d7 zsfH|n7RF8LEBTzn%ai1Qf6|> zaW_~id>uzxYa*X|xlC@PEx1IHU5G(>K-h6Ue~pO&6Usl7K>HZWAOlb48i2VxZ;}7p3k3QL?@;iU|_V#KW3eQB%g=A|1jJ?=(tt->=XHq(V+55&7FOo(b9S_;04;cIJ?!CC_GM?G2Rx1qtmCLlHE28Xf6p1cDh$6n((3lE{?k_&`~e{kx_g( z?H+0mJ`u#-w(cHb4m;RvI!x&tmxyOEJ`?JFwqNIn-Q&SV0g@r(9mo|8=#<|3JIJl` zKd0y!feHTqkKnQ-Z55n7V|~4|FdmQhk7UZ*(Sq~~~mY$r%9!YW%{(o2Cmv02@+cQB?SdlEH0_DHzRTM@7any7ae(Unf+#~43Of3AL09rzhsnWT*h zUSQ(G-CQ{81(RmF{Hraw)|&(K@hsvr@Iz5*%YwZ}nM}Or-n$gZqXK~C_@z2;UFmOw z;~;@L=1e?`mO@OA7}aJJ^n~akSr6oGAY)RYK(u6}nH2-KH77;!HRudI_dY zhtv3hGU5}dA?8d}IrM&gZ zFfEJ__X|n!H^o@+VlvW%Z7?l34dj1Ee5luxg+a-YqFxTnmB48zLWD()y}4Srz+@v# zhpqO^<|?k_AvvQ=>Y8V3$DpD?UA+i|R0(pgO<6KyhyooN#6wOdTAk9Y`#lMXmAZJ- zsRRzou+%&2B}6-Qm(QQ2$#XS`=WP7*=Jz***cUP(kYN@IJ_!e?Av+{FhS**mYd0FO zd#H{nKcl(|ueW47yvKptF*U(FKl@{dxhJ$}BKK^Yh45k>ww9lQC$rqTc8M%0p76MH z;M!@s^~?$1W~Kt^#?z4oN#BbpFKofRz_&xE-fMEZJ8!@qcOqBo^7G^AlCkeCMb@eJ zFH3*%@Oj?X8TP`|Xr=U1&9sBj_XmdS({IZm8U&Fg+%wi133^c8A7BV;>dz`9jQbJt z=2+nArWdFr)AvmJiV>+8e!4_F(1tx_L=qPV`7ZQJs5aV%=$CJEN#%P$+YZ7p0@+&m zUyhOf8p#+HMuj@9E4DviqHEV-H#H!iYZ0kXd^}M$h8`-0Eo4a7!uMOLsU)eL_HG6+ zqGv{C*fkZ%f*_k7evYgS2afMWhLr2N*YHxMVxobm!P;*aNx34`b;?L588vun25i0d zW4&H>bgtaZ-(5Y8YYUb7;()~sIs|*|5I`gLp541vlG(sfv zr4gp)P>l@OcX++P(bc|Puz8w;f=y8V9*ksrL{U-Z*-&~qu`8OCOj}0Ct9U&Hs~c98 z_uiCdNtcDgirAKQ_t`pXCiCrq}>*B)~j$ID;q4BG2IZn$k^an`6I z`q>xAht&|qJiQ!AOdL(70JZ1D;ZdBABaE3vAu?U)6^-oGJE8j;pxG5XsH}sT-;bpa zEjZAzyd0JT^kaV?O+DjvesGZoT80^Mz>5-*Q{$p)XRjD4tl&YZItEJ%!UqeB7&yv`V7b|z#e(#Rr= zyH7?k8`ivN#T1!!wues6gE;Hx=lw;@(4+?7mlm>`-fFi4PSG%rv4c6P8m&1OqZ^TecFyD;-I;sCU(N3 z!YU#xDtmGIY?HAnD%dJZEMzDy@X2+-wQy%>9}~ACwOf6`E8t$_X%+SxiCuZ!z3>ie z_eMG(#;E{2d>Qu#U53{FPwz(s7Z=v4VA9A8tXya7cCWm>9hj$^4*4p)^2Nv=Vay62 z3VuZAFDf|nRK-njpi=R8RP^{K6A#YozEzk?_p|%u2mqMpR!uu2H&{9w z@iU;PV%qMqmFW?8A^#8xUK*+2=8Isj)MJzLq3QY-GD47loNZgB(15hH5HKtVoie*9 zHpk0LZo!x7fg&}jUSt}17kvB}!5H!cotte*}cE_U0B?uPnv zj(l)vmp`AsuwDLk;23x`GjAgQ>HLYohoY6q(Thcc6Ca&XrfdGF!Gg1U2>qD?P4;09 z2a_nW;JOFhxvZa!N=5IaT=dqB(4%`pdbuwBvudp790FN|U^~xitq|I`F?2Hw1d-S2 z)Ih2u)V1LjDmkW1CYazjDL_=!MiRL7EWtXrkx*L-jA{Otk3>?M`xrw;1>6q>_gY+Q zW(cl=5~tc^(58MnWCh%K1+J#%fta7O2DCy_lnS#kUuhKhsS-NiNbojL%;8(;X3B5Po+O>$zrF&vU-FYpT6??6gs9uk=6;sfo#)q$H0cHg zpM2(XU_R($UAnzdJd)`F+&SKJn>vN|3n z`-Yxj8c~Rn<_Pio(!cZV$92gAS1lPdui3Az-mIs8Kc!vV)wpH(m+s9IwRT>9#yx^q z=auce%tjY%yETxQrUy)Yq_(^`1LQ#cFd_}fDS21i?Tm|BhdL2+3}4eZfxzS4DE-cT zN!p)GNAgc;Hv>`g00Zib1_f}>J9aFIr4joc;kavJzCm?ObKx3^Jss?-|>e+O%ENt*}61D_NTAYJ(U7}=n+ClS+~Vg@b9`aT|3j`dpo5x+Lye9Nr| zaQ;fN0CQw+f6oU4_15M7FEnuA0+>GlAs+}3)ow_<;kkdYp@;haKQ@>sPHChivDBX{ zgj^o!3K^Yxe4T=czX;I#>x{ z_ltSSi)ri9Key8ntl-{VP8)ViS(5BEItW5?f>n)uq^(;rA7h4io?S3thv^59SP%44 zn_|`nP~1cB;~v7Ls0nVp>J5zyFYJeG~1O=9Y7%i4v`&c@F zT18$*yDdZY5H@6Yf#W&Gx6Lg_zybQgdqxw)=PD4NvB!$ZU(J!>Gc^jGLt82@9#}v^ zZgFyuFEQA=PQh-ylFK$G(W&wtBN?G&8gUbDCE;d3Uk%9-44KWKNL4>KC$`Cj7C4zU zR6%aHmhu@t;ld=+#$OH8q59W%G#CUq`6*-C`N>e0pkVZEEZe<=tB!J+W~! z+>l;uV>#z(_~0<^_T+>V&H%$89)&?+ku#b=b7h;}7Zj}u_}0<0Qx4nfDN=8hFkVkd zQTyW}IFb#O5Ru!kvAtR#XnT7@9TN>UmTrN z@J|$ACs(QJsvoeR5{Xcgwm#dsZs$9h3+MIW@4HctSpQ()e~@4A3Ev#8qL^{3l+_yF zsoXqOn|i!6{VO7h?c>3>ij=|YYx+E4&Q&|P8V+gRugEo&=_2kBVZCP}m3d}uF@8w1 z`rQnzjPu!_-7;z90Vpuzi@?i@fK9B}Dk^8$JQl*)_Ji*JnKkD%m?m&6KvElXb|0%r zcDC4uq?d0iN&ohOK0f}9QOdT-Zz&-bxVMk7yQa`HW{-!aw?n6Z5EqWN10k*)j_>V` z7Vz^RE#Slh-{1Tm#+?(}4v*Emu2c2DA?~)Zp^ar}nAi`u*X(aYAxu#O60Gda2GY=u zjgBp*Tw3&zqE+DbB4pUZPgF}7*EbgiK@&dw_xac8(1anO13WlO)jvAnWJ)*qegq*2 zw^*K?l?&kvZFM}ZPej8kV&FBUwQvTNpI?^?AzqU3L8F(xH4n*x z4+IIzcBwWS&I0yYOKwz$t;ha53bMw_qv&}S^71!l$HdgJ_V=S+=2P%KQXc%z`%usp zzBwHSDRf14orik0|N{b;jX9&1+dfCu+0!s&Ce&&uw>O2Xy`3&AB@Zv_UxSrfsv zRm^#TDT2b}?LdYRw?CJqYBj#cH?Fr7H9Sa9QAN;ATwB62&~S4rM3p!qQgP{pU78r} zO)wZ$ae|UBz@Dwap6wx_u@o4`{1zgT7;p&A*A#lk>te)Y6NoH<>o&GAtf}i-JQn~0sUAJ7$dpZ zkR4?F!Tk<4(2ZRcE#IEwBN2xe$#@NwDBFe=ytXJ0j3o}f^2Ax-GjE_sL;YY&qt=UN zcVOn0jcOUE>2-ULs()6(szh|2GaQWV0L*@JLv9rm09S&prwS0K!|nk ztmzxg)?D*f&*(55kUbq&ZFIDS*I`1KAQn#tjdVcRAQZZT5qF!ROGl6*k$oRac>bev zy}F5d*HeQXA#zKOut2KJ)+me(`S!Eueug9lx+`$^N&ZB$-hucJaN$0aaM-WcP+>4(Hc-ge7=`@b|1HNJqNVOU1^UbLKA^e@U&qr4d% z>szlU0IK)pA*7{+NQtw0^EOLtAjc913gsMz4|#A%?D53rZThoS>GQ=91GibwQI(xixP%_AXi7BK0obuILKL06V1dk^t_rQs5__}=$oA3Va3)Kf=iGoe-a;05Gb*HSU~sU>QzgAk$!Tr#DQXurbyn9E71 z5Q1Sn1q1uxc7uH^gxijV%u(`I&?~X;g6|;8f;IbT5sD+;e4PB`?w`W|T%Zn)PTM5_Dz zkSYJ?m!Hp-+1Y);^!CdzhZ}UNDPXTw{gCy|WeklDUa&=}+jWCk`^|w#Y=U=_hdvdGhj*0+r9z5IeT6shHO&gO$AB^zn0pQ5r?Dh$`AY>&D)!a< z7$(NACWH$*x($u|Nm~Dwd|d$|+=>_QJbpSq9rDQPk#Bk5ht6<7pmB{mqv`^l`8YJj zjAmD)iF>A= zC!dZVpQ((B=D_QXvY3Hq!kiGz%z!Ul&GJUyUYI5hP@%?qLk&~6h&~Q!H!AXK!P=am zTxtmXNxc&NQ#*KKwl%8g zBa6IOw$UaVP_K;`8EHv^jYjt(h?+c`4s#`KG$r=a!gjLQS^v65fzk5N#!E?y(gh1C zNf-+pKa7m_bF74(CDYk&)bUZRTJSHIRAHJ9=jM4^KQp7TKKCsWm&in@@cL-;;{i+Nl7((1d1 zg~yms;4{`b|sV<9^sUE_AokXH)ptw70m1$ULD-U}i1E#x0JASVhrCXdg;&F0Wd z^h}+F9-GU;@YjSJ6n`}(fP-JN&|(C8FD0DNaKlnF3SJqKokg)l1YNr8UpA7xRYRjA zhR<()(*%#%5bq`zJ=P62&&Pw3B1Of$!mI8`GeIpgpwtHDXrcU+mV3A_RHWcV<^mX3 z7ZZ3gsb@H{e5$n#TPu;f4pZwBW6Axxv%FW$0_YKC;7N8L!C) zaQqxW zyU{{o!Z1eULPC@ZApmQV;m0I8uBD)1Pybe5X$&~v#_Q6>g|$4IEN=sh}xzb!=xjjQlKS$Y-f9 z=i#Onk+4Khk>k!-zR^-$Z{t={ut(YV+qNMPc3^M^Yji(<&W-=9jE>%|aU%BRNPnsd z<{nL6kqpa_o2kJAopwW1H@Jxt+zZCFY6AtD>- z)dG_NFDCHGFNrgPnHJy%dnu&0gVF5vL@Cq+9ge{THS&Cm?8bb0MsynQu zgcg5-u3jXRkU)>*<3)lB+u0pmH4`4X+<{qs1D8C@ks@?l#wAjb(iHh>=7hnD*6ZHE zPLGqKALsi_56G88l!+7I-$Ngyb%S;Ez)#eFOI7ZSpg7Wt17%w{)1{bFMnR6)#FEVRgzp zHym1h{}PS)#FFF~D~f9y*1&k2Ttk0h6y16)Py7}GVZtM>qHr}ieXSs+1ygZ-PCv@| zotR^rkAOuPB9O?oAzh>(4blHjuvU>lgDddB;rCKH>fB$K~!kGE~8HLv4q z_HfsKB)3+?t>PyqsbXc?shyftl3_h-Mj5BFfL(Z19ZUU@rqf^<8xBU^VB3YVZb=D8{3U;KmsUruP^)Lrq@dThU?di=7~>_!dB8a zfbHZjOOJK(PUh(#pCMo$u=GRRrr8-`T={12Q^QpmU&ZahpQcfw#;50dlBYVi8jNLp z&VM9Hc9aeUANo4=T1xJ3j6eF4FW+AY+#9-*ol36?)1)|m2D2FbgH0gZZ745IB;2aV zz2{k1F)89=SAZ_i4yl-^TMZ??Faee*p5T2p$YjBEcV2!Yz@JhM))HKn`yb}Y>+HAj z`nyYhft9x;pJyEg9Ipc*|5Uv-BLZ8{xd+#uJz;J&k7xy^}bS;^3-snah82Xemj(WSor{?NMVD(C9++&;75xK%b^WF>4@Pi?uc{vX*JKm-gs6&zB6ncr|8JH zdOmJ!9he`o;lr;Og7UiVH@%4AqWr*Nn4&#~vPYE%2bZ)2D4hs@zZHM2+m5VF14a-I z+mV0V@C>f-@gJr9a-5Fx66gQm`*Rp8^dFHRZlWR)&x&#v@?7PDcHh zbG`~L6!J73t~$%aHj1EFLQovP!h6dJ;Z!59W2MPqfqbEZ6bzF=(z@{^yk4N^p^@q` zXnrPiA1^@xQi~_m4mb;{xq7gF3&qiq3i0+U*vEWLlHw1%ObRGinO4w7fChWXPmpa_ z{^#nCJfyjUG1}^=@r{;3d@{SVZLQF=aBMG$(A9N)bHldeN$~9eZ{xk%*v75blrvN- zep58$&NCG{@tBeDy6YH%vSARABPrBYoC=a^mhd zlUK!if>2%_LxGXnaoXnu0$^HfBCa3%N!b)?NZ+Jut7yxO19}3HddQ}7vq0&TB2pc> zbvvWE+;Z@>h3NFOzWKB{)fu0`coJm%WZe4F6LLjwSYgoN`_G)#>Gk3XB@?$`y$8Rp zTuf%hG{nIVb7Rj9uGUy&GVDE5Z!q%_satWN_m7$TOA3Ay2j6De71O%z%Rmul3{75?C%(M%TJz)`Jb3u;bMC=?Cj{dMywk*v zmKFS^-C*f0iZc_0C`!TqUjb)QExiB-{LcQM=!r`h8w_W*ECH&jNd~h8WbXttLuvqA zh*{AFW_k}34dStbFGVV3lme5S|5cj2UgrnIV+Xq6@D;}V4VordnA!pf<7Pz>lA_GT zVFl5W@#q*pE#__c?$1vMv%H2)Rl$3AHPaYsXmE}H^~*z^;Y%07u$$g%*yf6u5>HcT ztw?5wuE88(|?T4BNEN8!6k4=!+25HTOaOJQ2 zdkGGi4AS}A3W)aTOSec*H~oiD5nEsu5c>LrfQR2%5XLLPL>_;~y8>%lv zpD_FA@jA-ZEVfU}-*X5$Iko#1LoUyCtEVjfm`%_@N?&YyPrmQ%c&4K8Cp9ED>sM_9R(*8XDsJj*ed*Tz`L-}f ze64_O&iIVg=d9*a(9QN`;4MV%XP$1X6|CW~&pz^Q1LZjqHt03ZAJcr$&#Z3u8bZ$E zDS77j;y5O+Wu7mV{xX!lhI9wqYO{dcXu<1I(iwgT{loTO;0#GZ>MZHb?Iz$6b#0aD z&8SF{?OJL8$*mQkzsn=G;QrI+2&+hVnld)eHOD`>Zp#1uSwfvtCaBqR!ZIk=%B3b= zL|tonMu$G8_B-Es=miS4d9WK~WDY+N-3(V1y-N&G&w%~Xion}6No9^I%Lsrhs{CC= zXPm(_u|Xc+q_LGvbiF~e$lg$iOFcZOCcEHLY1P?aC|yPlQ?`NCK)y<$9@nb=Btk)3 zt}#8!*-n)rR!dnxBlCZAE6@#Z0cglr6tk2?nayIy8;u zdOax2>`MqkI2A(?Ba|A>N2z%YU$cw5-h>>Xi0(!tzqHHdZ+{fSOe3e340Bxb8C_lR1hcOF;`302lG5tFQd7Sgg$N@j4gLmB6SMLl(ZXh~GWmqq>5& z@o?c(GdEU~`?3TSd!0(0fL(`^te%Z&T#@qI}wi4gWyJ zZMtIvK&oF_Z2cOs@v~Zgh^8nZo9qG<3}IiRFAXp2coDwAKM?iz+0GlQRu}fuZ4oB# zWeCgQ#^((dNY}s}wj(}^q%+gbn46x5pL2hLMV-6K_JfA6mzdsB2|_Cz1ayN1t&P7b zG1CmrS{;{HSrMDOmnCeF{s?di4FQRg$7Vj*gdu+D)NuWd8U5+2QDJwCz4zA-={}_YR?d}kiywQN4e(I$t-^AeR-{(hMWi5 z^h`QP{&?N|x30EXoCMnnbmnXw@YXoCVAVG}S@cG~Vs^5S3@IcGUz3MuXFlP5{#9EE zu&a3Y=aO}JrUn?K(Jz{-h!Kw&b7Iu?jUxtBh=uv9kC3`71SxhS0YhCkD#@cfOi#VYw+laa9FlmkYwcbu&)Oo>8KWW^)cTLM3K# ze;$IYdmP(p5TLQ~*@|WKRZ!_?r>}RfXkL~$zD?PTfR7ntEFC(H=6pQwQ$pV++7yS1K1V!q%%#iTput zzOYgw1o5P8``h>=h=#I=A($Wz9~FHcc(6hf z-{83rFrTX(vj3%oYDVz0rQsFwi%>}EPF5Df4gP0s?d+mKufXp3B1Ksv>dK~lmh!CA z4ncKHmlvSr?QrJ`~ry0*9Kh+SzW*aKx(ALlX?sEhJ}3QiPm zVOSm6YN&o7)^*!l$ZVbU~Odw$)=IX=i3!6%j>Q15kThh!g81M;-qg9kJZlR z!=Ca!B}v8YUO^+HzZN7IQlq)3fm}AOc@Z;D?2L<7@@2sxp%b$ao{BO*0_v?>1K{VRrCtdJ+bFSJ0q_G_NPX!Pk)-p zs-m62QL<#yzWr1KbEL%|ywfVjuamE;P&(@t{WXnWK~kHLQoUy6dL@K73t>m<07j9AdjL|HwU zd42`EYznO&cz^;=I~|H|dk2?~i^dy>fexUM&IQVp6k7rz2yQ0+yW6Q%_zPNz#+WlP zK?hOVS#v_yJ`RRHLn$@5@sU$Xw`li?r-fp_#Atubt^o9wopwkvJA~|g7PO$_xTvAy zb;HW*m4;km?R41}8(9DKhwc(yXNxV$Y>82!5%b?uZ5GcMDcUXMstbY#cI@QO;4- zK=OBSYqNF0aMulgnlF9VaZ)xTZkxrGhyqf+=Rh>(bls_9F?dH|GQ{HFc}7#rXtDz2 zRJn%Ja?voNR&*)Ev$sqg|ghx_?g6*JXFA!-edYkctB zj~%gp6aL=dz+;3`aX~E9!Us8_sk$$q1FpJ^9ieav3vsukd zuBHwCy{moSyudP&t{`ry;Nd?G6^-8}6l;{c;*2ce4p}RSJMw-hL%Yl4uY&%-o^GkG zBi01^N{H#{Vym-*_g0|~8!sN9k6TLuZ>wVhb{pO=qhGK59rvziK7PyIS;zq$N@^FjvMPV47|RIf@5;VI#3<$`t_<$*HFn-(t~Dc@5bK6pH^_I{ z2XDcDWT;-A8{0eQ`k6fvfrm(KFhZii-llURqb9uzeZIX5T>K&R@D0&9Ji z2ppQ>1Ct5nNFZ4V*w;w+gRv~)++dm`$pfEYJuFgsP~NATwQWSGmUk(S;8I^gt|b|d z%?R1$4g&fK1>?O0^?5XuK2$Weor>_g8pf_-_P!-V(F$@afpUhEDIrz>e*f?{2B*FiUo2+4Ic6b3m2W##lT_WsjH_c5pIo27z@%gF^ohMR z=eR|nnGB{1|93SC+d=Vfr1!z4oC?j=9+B<9v{MGmiW{@5oF8E=;LWT7k@8%%xK=y zFMYfco=g4ene-$Qdfs5r76IQkw<&||0`!;7Bb|Kg#Xc%hT-PQGL% zi%zs<-SW;2`G3fI?|7>JKmPkHn`36nUYVin92#a>h0G&luk3w{jHHB+Y(hp(+56a1 zHX$=)WoGZ|rO)@ee%I}~{^{1Ob?f$i?dS9HxIbcsWUyx>b+;HqJ4i*@#9=(J2eip* zq{+UjTRc)4LBT>u1Bp4D63uIV8?9x-r%mq&w>~Gpn9K3;C1}`mFe@K7t9L3vrozv~ zM~0tIcoiD%o+S<+c=ahvjTuiB9rXNVUeOsLK1kT~T{d?;)fMuZJo;85d$Zsb5lo}R z6WCkaS1t=qkDGkyTTM!`PQD z{B|Hsx*b7QJ~QXHVwGmybmL7n4_>b^Ay%d7=y}nG-@_Olw!i z1%41r8a|+_Ixwj_;$+CTTDWCgorAucVbo@t_hH6F!KA^p@d`S-(q}2>^^`OB(cm}X zlU~Q=fladAXP2p|f6hUjzGQAlWOeQA?%QEe|o z*AW7I`_u1R6C)#7Q}6xY!POhj8k%ZCO{mt&q$W2a-ZYC^sJzM#xIZ!cYbNf+$kW&r zZNi**XwGXFd6XIaNh~EB7fdTD*F{+=!0rkT zt_yqO6Al--b;)AEpD4o)#Y5rWA-At{0tn@pMzHDvTvvq1$+NYMZmSz}my?lCk^hn% z;sqmZXK;DMLfpt@LxyzVyVW1=9+9ngMMC7&=wAncmvykjHv8hrUxp_!O63kVzZG*N z5!*P3q7s`$coef-1p3av0C%JWtq9&@=&NRK0avWz1n}VVxjW1QbeC+TKkN-YJBJ$E zy>4%Q+?FjS*>2z+F5EN_0=d{#y`%V0Z`R5rvKdMIT`_FY2CBEEakb3@8Rnl40RjyC zYUyL|#uY9JS_xCo#xWjBT~_KxT!`g{#C>Saqx^nhBPT!bQnJkho6h35`6G?>>)tu- zfmamN(}?TeKHv>AuJmf4n(h91|Nj|{cjosLz7rFDXGUq>l!e>gOnl`0aI`Vy)_PU5 zJGV>t$)QKqIzq~27C7!q;`j zroLjbtT2kn{G#WsDLAetxLoxR$qMs15{JjQXOskqncf=wp;WwI-<(ltr9w>xvxe<6 z`MuG%x3?!^Z8gVQ_<$M*{f}$1y+kzVuxrT!(ffN)QdBx)T z%wd~qDBd#~A{D(lh`{M+d70%Ks@d!Wx{y!B6KFi|&JDabX5k4{j#uReiW!=8`>NfK zn7~SC4{hKGmZyuDJ`)wAe@gn5{gFYf60jP=LCnHkaF54Cu?K%`9oXsVMWw{(@+jPW z9~rPm2@{{}EG5Y^MRH>m@SvXPzIx@>ds{mh`&`H&)TvjE>jG*^^J!9R7w$jEQ+q>ZtJm6{tGju#v0#&xASs; zT(2TSjE%VQ?ZsvCi)Ow>96END=O@5OdwPF5(?$eE>0LobQiOabzo4S96*ML8{s{Sl z!&emU4kTj1`#0_LfH<)L$z*eV`3b2f%6tE73}ol@wcYIH_r4Uyy`@WFj2U4iO4jCL zd%O-S-ZInGK6=KDt5Z+hN_w@4^&vSC%f$m%BRnH(`@8GDzS#^z?Ud-b(x9_Y<4xyn zbg2ru4_v(!UXrp3;VOELPpt>h&Z+M{6T*ifIq#ZzAm7P|G_Jl%clN|zbWAI~IZ%Ia zL8YNj_T7cGNmvTnJIjWp0ZiQR+NWT}*^S}DG%VL}{U%(+XQ^M%onk0w>QoQ2@C9c2 zxClsg)V#eL?Jh$AMe-KMt=u3o6{S2(HX-V1TuX^6bix>1XlBydB`fjn=Rp|s@ z)qWc0F*W~8DEDWx7JJW1qNgC%0eb&&Y!rl`83&wC>%4aJEy-j9rHnWYsibh%r5`>xL=Fx z?LUE`t(LV=9!Hs&P%wuRz%=AACs$+uu4Hzmt!ic!k>R`rE^G#yl_ajQ8KFVJh5w3j zmXILaeNJkpFXX`zE|%Yd=$Rd};9n)0{jz7Vxzw;XCas)}5CjTd7|YLg4W^Zni@B9$ zT!F_HD+D3E$-c$*80K79vgO6PHff1NiQ!_wT;HM!vct8RUVv z1dhVG*H5_F!XLY%=lLHj$q@R5NCD|0sgJ~Sz>U%p67Kiqr-Nfdv!795MAOL7|yjMOlEJe_hw7b33nzGe6dDn!~ee6a|rd?jS&g% z2BaQ{@!-U@1amf#+5$R_xHX`aM?JEC`fcz09=WFmc4^Kf6w*O(n>6RFM>gSXL)bKY z{`?6 zBly8%tTRJqE73DAT8TXivfZ`J93zbr@+XS1Oj6*9g+jUJ$`X=(RKdkjb6vyT{rtln zt&;c|VS58f>(5Sc67?ANMuv!Dl7-?x8y~?&`WQ0E0crROanj{8%!h7$7bo`qm`_yM zgx2T5$A1kCe@u(h>Rf@uw-e+UP}N6~4jJSAKoTQuGG}wS^e}FK9Y$k`N^<4fWVAtC z9lPV7x#t264{zW8nd%snuZsy)L1VVqx=ukZk$K15?@Mm73_?jN0fo6d7Tek(YncVi zVb4%j*jugUw1H0X`_QC*n||-$Ns>#;y%v!R*r3nxX9K9})My_A zLw$wJ7|O_tMmW_Lyo>919ylrBui-;ql84jZdwX(|AB4LahyI@reUl9BJN8a@qJC$T zeDY(X>e}Q^FN?wvUX5vAt=IH<&QGz4zjV3Ovl6WE%WAv+!OX`-R^z0Xr2iF22(hPz zHGeKcT7z`8wcOGmB8K~>5lEKq!m1sPX_w4h04wqN`EvW|{SG9l{JoZxetwdDA-KDg zBIrPGvC#W^_79K?n$=_i(uzbX;O)GJ6x2!a9t1o{WmX~kw*4~`p$|zz+c7k zBg9@P$2!vs$q=q((_hB%wEQoQho{T!P3{#mti?)VoEgExM*FS$v^|OUWOwCkSKK1> zVH!o`CZo#fGMuqLTm$MnfLXAFcwK|ikFG!$cHE(*tL1Z>JY!K&!RDvH8vmD7iwuyWU{#uH(8KQAPjcW2!vvid7)2bo{an9`n-RhSM>}o#DQ( zZpnp{DNziI{$i#Sfg+5cB8>frN1y$%_1@#WMq)x7GP37Z2|F&BP71{>0sM}Xp_Mb> z&AklL4&k^vMTD2BpWHZEb=qx7$Ywm>N09d=wec-Bui?BrU{Ja&0}wS{Psoo+QjG<< zE-OOjL)pW%Rgb_a0-~^83y5}I!p!Y3*L%=Iki3Xov~z2vv&oibci{Tq=BJE8{;E3f z=RXB?dE|dT<>@sb`0kbw2O4=Nz~cDOqd?X`YGIq;;!L3y0M3>9_(%&BL%a#XS9!pb zv?Z8{JW2lMT2|1-v{!!5A=n>)suq!~Rw+CubV+%6dVe|LuEaGP6RKqw#nyAlVl^^IY8||^^coi$@0mE3bxz?~N|8sUw192lTzTQ-R()g0QtJoELNb(hU z5uB8;4+z7&$-)93!pORA=V5AXP#$8fI|5pzL*(AJMKH~Pr?`FOTD1lS+KPdK5v%7u zzk{)D!Pvf~0LxFq1zHL*V9pNKiKo)7&djs|_R%sk7+`n&V!uuB2h)=HSu;lOf8&XC zU`?FQNA9jpzAIc5b>eLZ2iOK96s6y@ucrJW?FsZVk)-c7bfrlDmqB1cDk(loPiU)EnE zeQZFy#5FhrHE)2E>fSYYct!htcrtsK z%+b-mu4d68Xmb`u#pj=ycW!Xoz1PUtH;=yB_pHTYowPy#{FY);`KDq~ZQUev47I=rzq?d+dB&RY# zun}l-0RE&z6u)>PTTLD=4Bgeqhcn|R;KBKeVd@egmc?l|J7!q0Wn$2KgmC;vw77zf zW^&zD0-lFTsb^H86?mR=7K)L0FErFNzJ}-|zB{A(9!Nxk)(?Jt)j6+n@F4M)!-t}2 ztubyz5;SUlHVh_b*~O+txRmBKUA_`uvLS3Jc9CLVn!WcuxB=gqDP#SmwzngIZ`fAn z;aOk9QS&tUf)48^(?3L8yN}~Fu3^S5&)H2|YP4S_M;heMA;D)Y*uXX^^(^5@4=^m2 zVt%(eUdKt^cgh^d+z1D9(@n=ofb-n*z($4)e)uOi#Cxym&$Ly}4;WU`Zs|e4+ZCHV zUtiYu%$Py$Am8VNN2U5OW`Ahh-PbDgA4%!QNyVwr;VNiaqGCwX-$C?GP@Fg@noL@{ zBwcrmJN<&4_GkL7@ULC!gx)@M$hP}ZX47*&!)cCnKZZc<1pzuQ%6 z!~Mw9kM-DbABWh!#Hz8g=f1Ix<29*=)z5XlbwL$oo}_qUyg=;*D|OO zq)P@p0EyXMOS$?d>mbhfYdWP6uJrz~#i*11@Xa+J|3mZ%?Wfw~6F{zlGtzH2E4$Ts zH0ey`yDe?j-&z+@!;rI$dt(YW*}4}QW|4R{7Lzu$9qrtk^%q>G;c)ayFpQYOK2Yl}~Lv{_p4~kE&>q4Y`1hhfLqpz7h8+%=nqV z;Z+hbkh-XwS^u4u5s+cBKV^5(4T(|((`d{5addkR;-Fge#M zg7059 z;hE4kUC1p4vO~?M7_Y3i6+`%kbr4T29^etdp}LXugH$*Fn(G`IWc3irn&TBThLxGM zB!&!oTk&##23Hgg95_l`@O(5ME9URWew}adv?4y?*79X|;Kj)x=1x&jAF90C6&S64 zmnj?hC7_dMBAur5!A)(FoIy$ZUWsL=J22?}T9P49n%g#qQO^Fdj)S<(|F+qDbO%6z zN+5s8As1FH>J7(}Hf#FCqCZ6j!eMLUBEirxC3fk^9$IQFCxSk-mK<>c^uGOwE0eTv z|DgqZMlx`;y#hBwGvmNtFm9Y}Y3L(hMM%d>GsB|I#7lN7hrv@AT!O&ywwOw{JQL{* zM|x_4)6L~J^j_2lmp)>49A0!0(_fcV+ug{pH{F;wJ)+{!r;&)K+Rc8f4cp{t=jjh! zn&%{k@qL!o6@HE{FY@*p5q$S#HDTev^Pc#0#W>*TnbqjG1h@cn@!M?HLA>WG)438E zdZp0EvI%(mS*KJ#_kZIn`g!eJ(WlA*n18)*7BB86h&wIHP=4j}QcYoH!vyX$bT(_* zGvCUZMYq2wtNW!~Q_3W7^mNV%$QWcj^FJ*+1_~Pv#gd=;Hhz{sic=eE{MgcH zCAjqlo$+A_Lz$_5fES?9EnlBG|D!PySG76m-BY1J;R|kR3Dp2;X=AmM6)-%yO^BsL zzktcH=F+15D5Rudf=HNV`d>!dB_?qmRyiF_$s)UbIk{!UI5!ApPlMi0i*3`LlR*agseG>{OhQ~w90)6NV`Ew7(plb1t zMn1U57bnYZCD_KVxIX>vf4k&#EqZ?b{8X7S-nsF{H=a@LoO}HNi#>k&Q^uA4*BSO2 z4yG+OQS9EU<)bUNxEijpZca2EylD>ISPimvHp3{#T;;#I8hY@sSNPh``_KITlvEYG zSHq}AeGRnz4Kmz^Q}Q!>USywUx07t1jWh&SA4s4eahcGxE+hpLS)^8nHxl3X$AhAl zO1ZTJ(qQ&x40{5LXfpRXHbV86McPeH9We1fpQR6gGTfu0B%P`EVjgobi%lXU7bZ_# z4<*wetm$%A{xe3tA3bl2Y4mYlk(~RhKU|~o`nTe&;mE-8{`y^kk!JJ44GxjjJNRs6nGFcMc^%NZ?se+0 z^W%zM0Lj6E{9$agXVt;U_@X9&CkXS67bA)Km7;9s6Rmco^e{kzCE2+@2rs|Ww%wXB2a9G)EqOls9SsWoV z8)b?NflZ|1gtGv#OLSilj~rRKi_xxP=N)(qutDcmAiTX+08#VkuI;DLimK*7hBzrg z<3{HI?1#$0FX(LF!Am@8&`xDfYsu#3Rrp$q%A^)XJ9})V-ei2Ux22jKBCwko)@%pX zJvtuB6Lm+Eea{?iDBQ4?{pin3RYZ`$+OHW<`!LrG4m3%Hndk&zm$Q`btjHaJQ0#^q ziX%yBckPGqYIE70^VBTy4)fjzm6w{+y)B98Ns&Fd@Hd zohJ~XUw_-)W0Yj#v3D@c5_mFcvuET-ECF6dR75U;zm9`W7cc6b^mINvUJrUs6`jKk8lMb= zK2krgUx}0&^Bel7h|Wv0UQZ%K9HOET-sHqvFGiyI4T>kQR3RjfuQyN$H29z`;qYh(QeojH{< zV**$7Nkq@0zL**6YHJ@WTr5dSE;$lDX@FZo9x1|g-mqj!GWKV^{Kdlq(X{7XI%Hz@ zR1U|X(YV>KNcs+{AY7~amMgUoHMo)b-?L9W{9xXWhwmzMy zdV`~13g(`}{wl-nNB-?ik!TvU2SxIRR$0?SkMdqRvo=lR>~>>v9j|*0KLm%(4}K&b z!cKiJ<37$G)^M2dL5fYQ3h-d{NkIJFI}@y&XPKSd1xypa;(yAOmDyMz|= zjduSAe<1IM8ionEMnrBA8TqIpMkxu%7K02$O0~O71Gv{EO#$bG8$f45)J2v^DbbO; z!doOL0yz}NzlR|xR)!h`+y#Ui@TyCCSfT^d$ZbT@elAl9V2GoboqO@Nx#MT8=ltYS z`uPp{6~O1aq;CMkO@xuc*lnt@XxHT|NX{MY$91Zpru2A{0CS~800-2RC96oGi$6gD z_FJI=w)Xy~+mV2Ey~@4^6qm0yt**Tv=!W(rT3hSpNfi3Cdl z*F1}yW#-`#`t4cJ6CJZbv>)y6ulbYblik983*Kp`M?|CH6>v!rqa#&tm%Yhas0|Zb z*FTf=1kQVaVad8zX+DShX@qYl6|%YvARPw`-(;O$H-{);o(gFt&=%}&07s& zSH6Wd>@P{(x|l;uEv@8tDy4GgJ$SFt=dpIk?vY{SwEjozGf_A11C|CY=B6tknmEM3 zezas`x}i>f+I~H03-ph?0CQf{1|{?b&ZP|3NgzX8Ejz3)Ac8nsg}a#P@k!}#jzi%F zbL1Y$gtw*z)nDqGwe(s=q%Fb(NnBCps;Y+2j#|1uHz11<7 zZ*{FKDO52)WT%1FiUr7!+58lWx2dOX4AMKmCfZ{MZm!|k1EO>g7=0z z#FH7nwap6GSVhvA&ikG$%w0YxA8_H?0NhhIs;T%Jw{xTXBO;gi;4d^H_s%s}dsU!$ zpYj-~2)E*{uzD_!Wqo<-HI!9IB7B-C&A3LiHF#y`cL4FLAU!j!`sbFW|6Y|u7kr@m&5`EZs;hu*;bqrKrn^`b0)q)LVi zXM?g??gaz-z27e5#a8KDbQ6oNlZHZpddadRgWzAx?TT(|B}^00f27KJ&doljd(oQv zhD08wS_%TVD|bG_O+!vMb#Jr!C0M=f9HG(FDOIf9^p{IC0~pHj>JB>)u#PjUF|q0&ZbLdc%qO zqcU0F?9bq~{T5PGX=63!{(TW}UUO4yL7GfNP*0H2pPib>=G*u>i`r)aEei_VbhB?Y zFe~k27mfxU6dG7}7wB!3|rjIDcEqrDX+=&HZ%ndD!JJm5*#Ffg>_{ST66b>2*T^l9F zo{5Ue!Zac;KJh$b$8qxATQJ1D2C}-6?d-V4J$~wHNS}|}J&GC{;p`8zv(nBzyKq(t zPTa)^&~kPaGZSMIjINZjdy)W_bHTLH5de&5oUOLvTrpQOh8}(m&dpTycVU0{JQCb;fW$YBX04V7D_hgK1v~kYA`lY0aWabE^t0Wm6d)>4>k{ z{}8Ew6>A1pI_f{s4A#5QyziweWc0B##JBd)?T9}gR3m|eHf&aLU)@!SNR*rj5XO)C z_CpOROlU1Bdiqw0PmgT~|AfLi{2 zX^0)i){r_$jE(xi<1leZMkx>BXdpRV*T=!CdGQZl5gju?Yn`}&gE0(r`9QY<%o%y= z-KD?ZV#~qBqnNmI^v7q;4V}KCTln?2nfdm;P}um82Kopl3Q;{f#^vX4&V+wHD!=pk z8oeD-Uf8@|4DB!e-5=xgrONyd`KMzq5b1v99oR`5_N=Vj5gnja}oS$V+IgK!vj$mr#RysvNxg^E ze@ZLI;gjBcria_u4_1t05jEx(#fCO-hPv@Gcs?Oc-8DR1+b>ci&dklt^*~-whwFDrF({9|){}wpo!{qpR0n*kN zR@|HX?K#BS_guVrI7!8-2{=q&eV+M_pdS)zy<<;Z>3I4S7+LZCm=5JhP$*W=*P@^R zeJwqjSwPi8Tr*PZhhCYahWj@o&R#-HBrm(qzna?qWpU>>AZeRHkJx%38|~*90II7N z8P%cb2f(6@x)3&yBJ#pC#gk$=IbGqf4z>2iJvEGaw|+i|=J^2A*z(btNDZ=h{imRx zlw}|DY_@9p^z7i`jLLB6XzoI2beX|sDfj&cDL-kc^?*Au5a@KJeT___fXHEe{HnU* z$}wG_g5K1U@pjhCK>=ey$|S8m)r-=vT9ZvdF{N)es*a#S55Eugad-{}^mLqGYw z$}J{TVri?29W+6xhZR;wqtAA&s~%)rZ_g_ScCauyFUsPKvo&6@R@-{YVH#258yp5C zbOHXQM=e~nn#w-8NfCyg!}^n!blp!Vk1k%Q-yUKD0ola^0JiI_R2eyC=I^t*q;T=> zSS)uKuyVE4CgecRpN+_d5~=lS4RYj)5wjvcd=FtRO;bVV;4$4aLMxx(wz`n?1^7Ye zR_sA@RD5#6zWUa9Pobh(eM{V{RFKO-_Z)$L}B=A&j3fKGnb$ivr!qH5O~Qvw0X+SD7s>J?J({rbSqFh{1QfGNCs z70s>kO&T^>Kf!$yJ3!eM!DT+Q#)0#*+8VK3`=&hm5@+}jJgmUR_wHPbD}cfvaNteV z2+u3ltWTkP(|$qO0IMs7f1rrX;KGW`_g_v~?Qvpm9N5r!Xwb0mVrYkC=zXGOA)~)^ z7oifGjsEjYGX~{zj?#6+z&S+k*-?r>oQw`SZ_vHg~oe9*5p_K%|t#^rB zZl8{_nfQ*ki_fGKICn(|-Cz~dMl$=8#VIAM94z)_cV@`#X{upnk|fuE&78LEjh-zR z-p>%Wzonxpk7+Y7VNj|FyDa&;|BfhhD*##PH#%Nyvx$N^wBH;N=yAfL&^J-5R?fYB zL&aHJnPG2Xez?|wiD|zLU7LRthz7r`Z?==MY9C9(g)6*2_+E!s$!JW?^@bq8%6Sox z`xF3&2#7hz{E0ng>boGa zL61?BKS5-1QfwMQH{2GPI$yEo`5T*n3O6pyZ3b4=T|v)%?u;-FJo^1dvRGT% zD1eNrL^~-QiI<#vaQr8zzK$(J?g;#eLVK&Pdsa&sL5=l3GziE;=-n3R+?yL{MWjpq z8Z?O-vsqygOm@ZLyF+l=ANXw!dvqk@DLWZ_y7Q+)9uOw2g?U(7G+B55GBd+e>^>W= zAo_SahViWJS5C~sY=OcTpfuStmVb{Ibgtbc+bZaTY0z`pJ`~BTOQ08qWS%Z%9Lnb2 z8S!EP_7?ECY2581#@dCrUr^>;tuhPPy^J!-2&}aTcm{n+*Su;iV!L97P^WhWesywH z>D97$rg1px-8a3jawwWSP3?%t5Vpy{GZ|J5jT1jqnCB+0u;>B^W>_^DJ-x%l9*8;Vtr3VVhHoOM)m$1Xsqg&Y; z!z_IDo6U8@@o_W4kB^)HgzAKKt@KB~l0cBpiBORDp5*Exbq>Dnbz;2aRsGhixDx`T zmRAm_z+9+tzYo-cX>gZr_owTd2I!SMBsp6JxLpvrWFu$ndw~i#vNx=8*dqWGvE(N7 zr!!O8!r|OHAh7&v`smEIrevn?up*Plj%ljaEGy7Yd&&7?(JRMMIf@2761>%vF=~ISCl0u3ogt;v%gLqOpdst zS?_Xy+nxI|SdD3QRnzE3)wpx50A6eZVdAetUJ?baG(E4n!@1tRrmf=IZep3V*ZCA)f2JP=la+eg9gu2k-0(-WH0DWJbbow*?{@?&b$0@Tm-k8w&?{cz6~Cz|i<$?pF}H$dQfBjuMa2P>tCLK|HM>q@l&Z=C)fA*GZqTCNqBB=Hb+M zM3yliH9n7n#Zl?L%G6L5qeSz?0rkYYy@G;kS0%av8&xl6J_yaPAn2LyU_flZP!@i! zjQ05_xf8Qb<^ML7grj zqG3lvg1AX{7|{!OLbsc6D6jnrYOl;ib$5H6s!be5GbLrHjal*7wsg#<7}FwuGzC+K~81Z;0LhM%rmkl?4@ ze*FN1P|*J-y$JwFD#RTPf0KPjEnChJLp%Mw8j||1NLlnE0oK=iRfQHd>7rF9{AdCw zHu9g{+drAx%+R@!aBZ=%W2$hK0g7andJ8t?*_R-$!X0g2kMW|ADuGxp;8q2f=|Sy% z1u-V=B8*6$9sFZnM);Hl8b+_ohl|gz=XpgB9v@GWS`2G^`P)B&=du<1rSRnT-)PP% zhJ{JD#&k2bWxTr4)r%`~W_zurFpmOB>uSX{k z2Z~$$desmMg(oPIp{e_jn7&r~q{^7u4q`HWHs9+K%b5X2*cB$WtAq~&U0g$~N%xz! zh~qWbJ{i=9<%nrggh>g@Y9|c7BMwcxt1K32_l^X6Nk=VBfb68cCZ2`PWqx$S_=m8=Ttme;#)6W)h|2sI}43~d_IFFlrxdR$V14`$CvuOf_tCD}qGQS;^%o{lZ z)zGqPkoe-pC7I0kVbJlrK-|>q<~8_QugWAAYXQ39K)L^R4vs75DS=S?Vp?!FaD3L- zD5zWb81_iMq9lL-!pCQyQUj!EDokL}@6Rn~4b5_BrlUci5Uve7E*E zNEVO|M*7`6zYaLX?8`EIR0Qnr;?p1&aZA#y!1~mtsGBq#NW?3~>?9M6X#aKe@UqdV zbw)Eu5zsH7Eysx(jo})FX&j1_S3h}9g?{2}kshGF)hh&4dH<5)JE{y_+tqvr^Y2Yc zpO2WA)=NbO25!7NanU@i>xR^cBjp8mQMS{c){Qr6-JmN>5{hW|{ofyR)1>r9b8bk}hRJOqGHmKa+GY@&uJ5 zHw#(-udY6U;L&s(zzBls4q{)OytKVC1d_2Bid(>C2qqt#ZC*rN9wahs?9ZbM1)2HrY~m2`q8&JFpwAP{TngRH7WJ{26>K zlYnsHF>yUVzQYS+NSz?W(+ikzZMF!6NLL`qNU)oO+_jjr7FhDsXVid76u+WY3fDXE zModyJH-ixi&R#1M8YM05B|zH~ZuzNkK*JZ?@uZf0k)_&yucjDcvThK>yr*>chqu8M z7bAj(C7FLRRhYvpAAoI$qDwk?!Lduz2iQ7(uz_GZ zchTZp`Zv(xMHL)|1NzRc29mS;xcDmmp(#-}1KDNiOBnazhJ7(afA~|GGtqR-^Dy9n z3TzGu=qVp`l-;WrIo&^xnpZB!5U`W<_Tv~cq8iI_t$w2j|KNMUW7V_>$GsS0!!g}# zDwr%?njM34-yoh7nqapew-vW{c$l4&>t+M9WxF zI+8NJhZ<(G5di-GxU9=f*d%@iq7s{nMRfUNgs^`4i8dFn#e=4fH9eY52H^xvVP zlyUR-<&%>cZw0y9b;5pgu>7YT(xSf&khP52-^3E^Z);7kjM2bO!=EtTzX$Z{7EP*n z$zFZlC){FGXomxQLVVa;X5-k|6Fndx=BV+X`xZ8&%?>}@Ob z#h?-@g92M>46CJwjx934>boG`?`zR`R`}!1JJ)YPsZ3xpk7Stn$ z`o3aSdVDZ2VdX>EiFnqyI)VSW!B=a)eYb~;F+V9}?>W%MdCt@2f z(Ghrl5=coYn_{dp#I&y?mcd+fO%A}4!qP#)cKNDx*?mMhiE^ zkT`4?2OoyDw?eZApW$-kP_2e|E?qXLI}@bd4BEH}TRe0=0*7>z7F{4cr+5x!hJ-3` z)~gce>fn|HA1pmYWr2-ER_hg)qkU7DaF}3DdHXLIro1b7yy?|FM-Q#UzSDj*@fDHy5-*#?!}|$_!QdW zLh)Lf=fQi7Bc~TdyR>kJfOkvh)+V$&^P{=|PERux>+RA>w9AW(<)5jP=pCTK4Cc=| z(a-vE^ZoZsu7yxDm=Hw&%g_WF8SFEj;`s{deJ8sG=Q|3%_Zz}Nw0xK>=a@Ow*vB{6 z=H^Cdb)Gp1)9t5W&0RbFtu*1K&<>f<`{c6LAOc!8=6JWX{l1FqT{nK*9bXWqEyoXi z<%TW@2)PVq#g^143N4l!V}_(eciD;TALa{Yzf<%RM{&}mvV(1ff-@ww*>wQHN%EBO z^?oRB1#QmwtP(NjP4Pn<#?=f*!1r3AwS&)41r%GK^$?R{WNbx3(5uocTVIUG#wfLM zpB$t`$4Jj-!?Ktlt+zPqSMcMtTQ7&gAoEUcYXS?Oa$MyVw0=yMmU>9sM({ct&Y3%} zJ(U4SFkXTydIOXr7-fY|$L{?J*YyS$=*EVl_#`Fsayu+foCzVVc)4v;f{4N}%?G*Q zr>-nC;i2B&qd=h~TdK3&8&C6){#+i#fbc-}lZB5^^9o)~pU*sR`}OYMS=@g}Gf5WE z&!;WTi5iFrTdTTqDLHfkMjD`n`|5%1w)xs~lHQWt40YYNT5}wmTPF{S zU_j@0WDzKPGyv0$$=zsk(u&LK#0IUT{BYRknX~tQw@Q^XRbIUG&C> z`$*XB_Lmgsmc*${W+SjOatsD%B^kJn2J0iefWI2WMbS1P^6M^=>265mlWm>*GAG=|rfR}C zias$qG+_h~l47=OUCpoD7QAOnfUsbt&b?n=X)a6n3!`iHJm#R}6gtZVBb}oruo-69 zf&vb(ILv)Y<);cp)usfv68{;+gmHd_dJ{cW-YXCHhQ+53V*(g_CT?x*D`We(ux9i9 z^Et!*km>>lyq8%&XwawhE?-)kh~;_L1wD3Xm&)LUC`b`YUS4_Bzf^hicw!fPWGb-Y z0!E)zGtwGkT3{PfmPypqa~n2m$%!+Y2?ZvDT>3{A!H863Vj;VWI$H`svpgq?UJi8= zf#yVFlTsmx&oTKG{OH^gVmS{c7?T|Fm=*Z8iO}#TqQ-D^0rdt5Hk34CR zx?HX`$p*+Ddi-s{ON-(u7X@+xZ!D)5THCB**Mx}FWX{wESh7%udzfq{WhNpQt0{Z| z9QUK5H4v`a$Is_#t+;vZv4iAHxZGLrn8KNPSMsl_qKu^gQKTo)nmEzgsMPKup9CvJ zxX6^CwTtu%dP{m&K=NJD%lD{$e0uQsj(RAMHO7-qN>=3gNFYTrraXn1uz3YN&iJH?wTFyi zMjTe&3{SWXF`R?Md?m^>qQsP?1)|KTVdT$hgJYQCYsIOM&~8YmD18A`vxC%d^?Wnq1l)(yWL1F>O!0HU}Z zy|+N7GxM}2i}m22psb}75bJDzyY|tQ_3?p^y7|{Fl({Ho7ynb2e+2xg_v9m6uipI( zB(mvLEW3#in$86NR@I<~6GG2G;qxi=H8uw>>UgG7WBCjmm2Xn^5B`8WH|yVjD!s42+irDBlZ=WcL06CrllnyrbuLJ1-x zAPeC35HEnML7vL?$0%BvON|Lak$=Ee#P$kYb3+&!q}U_c?4|j_#qok@wJ;J zz=21G-s~YX^mkpU1e%FYhILx)7$N}BBx6^U`t0q)b7{Ng=LQ4u6!`2oGMdRFlq=3U z2u0f-M8tfgH@iM5!-r4MoJbj|Sk_UTXLQM3{ss_RJ*^8tgpH<-h=U$zf{ z&LZT*(XfAyW|A20pMx0xcO|+BUTD@|LGcu%!yWpCmY_ct&UE2bm;1*2^tWu;A2&PM zaoKAHt8Qt-0{6tFKZpN>8(=Pxtz9wV^g&mB>s#2K;xen28FU(*hmwDtEl)N49prSN zZDxk=mY2JY5V~9=2sfOa*QemcSx-Ff|B|rNeU&Ca{Q)BR5(r~fqi#+~;VKsr)}|5! zmYWNH^+UROTr6@vk`CR{q5HDr^QnFvyo%5TYlB+)sY>Y3;f>B;prBX5r}u82CB+1% z@lThBsOJB2qt16E#)-`U06-Duv)QPSAcyo7X)i@bRQv2f!BzPBNt zr8JU%sP^L{4UmSb!iJN;SdJOfWbi(`5d^W{x7E9JDdvt|KmRA zILF>Idu3!RE14lOQkj`YDr9G79#m!|L^dINL^kIT30c{D@4dHkzkR;H`?~LcTwS^@ z&gH#dujlje)MQ|DH!{&|Jqeo~f<}+DDO*$J+y5nnQM{ZV_bA!Kqvke7mCk98x)mC>S*p=1^O6Bk`y2nd$tA2wDj7S?q_Rgk> zTREN8Pn_3^kOH=xAIW+kZy*hkk^=E>C52H*kF?^8K%t`Gre8@w6JyI4sF}?0rJOxb zgRKzniJXqYO&Nvza5g@c8Hu}a9|4BaI`^lLBq5d&io0-JGMo=4A_6!_HLEbm3}mg% zNnr~?5+N^QRb&y-5t*fOq>&C$+9h`W8Luw7Dm1!%_OU?ISU=s*|mQ%I8qEL zN*3minwHX3%2;}{@j|C$N-`$z0cd%O-!ex9Z7q@eiYP&cNaz4^Hityfg=Y?KxCp0X z^!$QJ0N1E;8l>~cj3*KN#IV(zyMCbH_@hAcsx@A;Hmpo!8Ae<-gu_u5723yNe0fuMf~5!5g2TA_;d|lhj4} z#C+uDbhQxF0j>$nFof#!@TZRp=XNw$$AqD+R8D&FVLQOja0uL>#>d6MkrIg=Ow>+f zg%?o2c@YvW4X8x0e^jcq=gRE44c8^X>KV#=%TBIB`%qB`qq@)o+R*|6qh7#tk$$iL z_O$?#Y)epJ%BxG01Lj@fFacGgV0{Zd{UIy*f|^p$F7GH{W~j zvG}Z*ouB-lc)#GLQd3U)&Fz4I<6QH;H~$OQq$iReL~tb)*Djr}RRID2;)nWd6OiH> z4?;K{&RJGT&yxsmp~P8)>S~swfIl!gE8fq|CO&-F!N>^<9ArgjD3xl^wPd3O{W$jO z=Y?*MA*39l|KGyJyMM3ToE@XMJj`(ry=Wba?=UN=q~9cb}H+OAtE zc(1!ag{G?>*|&hinI}D0->2nYn?F1eMUJ>oj?_7Cj68cG2dVd`BIXRDw-a6l$N-e7kLA$-cV$i&535q!$gKc$SqMWg!;z} zd*G+Ko;uOH*i9kQ>J zlx0*~_-TVgo$Jbog`+)zxFQ$E^ee=paim|@$j^CP`%wE8Jpe`z`X`YMn_Q>u8kP43 z8Eo=rar>zemL5)lW^Cqxo?s}VTK_*dIf<|xNMO)q8+}$m;isZ;ZN)Fxztz4#=Ya38 z!2%2js<%&dm+8Xh8_ww7Jn6}XS2xxy0F(YRY_kAJCo=^YUM|ehdj#vH9*CWUH`9WE z$0i!fq9B+PdFWkd@cO-2Qm}UPg;uHu=4S$Y3@iSuUlMJFOt@Y%WSUFy&h?Axn-;WImEK5OVV)UB3Ohg2IQh5Ucl$q1lC;{VFE{L+ z!AV+rL$w}fydvzD1kI@bjRe$Vp@0*XcjubWp9%_(;A|o+GctGak_Bnf1I_u7I$1ap zZ@L2%n^74w#Ase51k3_ogLRAm4; zRbCQaq<-T?hoeoGi*SUjQ`XZTOkvU{efS;&&b`F0l0-% z|7i%oP3TmGC=S9?PfDx!&lZvJA|W!_21XO-;G*KrP?tsWIFkUOTw!Fw*}r@s=%EI&%vAUy z&XuVMt>mWoW)6`^5zy&CRnPbNkxPjRyj2p4R|t$(Xz8H_H6dyh2*W&dub%;h&W0}R z@DtbX!Z9E0Nsyi*$j1mAVZs}=Utv^uJn@lguYH!jU4V{sI>wXpFyiZcJtc{IYEOPR=(ZkfrSZISwC_VK+@f)`w_uc zf*qb%-dM>01?I#_lm5AOnUH+}+1>KS^t2i8U*2##sWM1QV%sEU@R66j3KM4qdXhcT zy@psykqVdlGn|`IwMYA&CE_kMlA{4a^|%!Nj2Zrcfq@;<&Ls5|MNmQ| za$S7k0VS?(Z$a}aaJLo^L?jF11rRVAi+1P00^dj*UVX3@3LqB-iBh-H>G4CtaD9H` z2;{~=QZ&eJ%o{*FgW6GDBkGt>z-jv#^C=vLYoCLOZL-OpEhpo7#fR+p{wtP(SNH=h zxEhy1>eOvJfxP`RTQ%;YAFcF3j?#yUQvV!+ewM&Z`Sv6ph|{#+Pbrgxr@>5hf0+6n zNSdf&hl8OKxDFJS0Q_KquEpG zTK$`pi`fs-qz0BoXE1BXk#1MzvcN{*+4tjkbyqc#p3uhXLU|WU-T&%B1*8uIfxttA zZEHuL(g4|~rNjVB?L_?ew*kjHa2tbtd0n8S$qaX``UtdX@vPw%a~CfF)xBEX3|04~T$U zk4%oKgq!$pwn3Y`_tx8GA){S)k#%Z^u(5~hIiimogZc0P>eJs1%M5}DMqupD*$V&_ z`?O)bcXm6BU!wAY&FNPL<)N1IIfI9imKaQc$W6Sk)FE>y6jpZstgw}Km|3PVbbY43SxFv$^c?g8=G%4BST`r@td6%PBbjWE{Aso^ z5;e~=wQyP%T}!D+p{B4WW-w=Y9R&%FncAmMp15C~F3a!#)mf1sUA}LAwYe|n>*axU zIHux>+C97r+t@;@MzLf+*eAigD_qP7sjF z5c8RujnzbG9>EiC!xmNkJD)N-0aCca{Jt-)p&7ihZf1YsR>`TAqGcW#N}>rdVk`6o z1P7Dwb3*n1*}_NGw#)_<6j6DDXR!Lk0|F<#2uLUPrxiVS4aE4IYarlS1Uwku>kL>J zEr|Oc)k76gVhRHlH_p3iAW3Xp{chy-cW;b=H@f1I6^hMz?H^%j97Qa(mIJUL;`2JZ z?%fv9$Hnmphc`vw0%0(iG;xQFfUzAXyu3{%foEx}*iOlh0j<%#M1Z(FLik*?D}!vN z%tcn59CU~904t=pxU(}9LM4(;F9LN3I~Ra0@K0Ag)Vd)TUAZ8975vCQ4Xk`wmCbrB zAq*Kw?Q-aoAUija>Zfm(@Jo$y_k@fu6=6_rurI)vfj+?)Z^^V{-Qv5ze?x!Q>Doy4 zQ+5$A%o>2~-^4=N0DtqeTQyj&{?+9_V4DWii6USMM2-StJOREvyU;$D^Nbxlp>&o; z$)4}_g@{Pg52+Sc0=O5!yDI_6x*S>-VA+OWVlnk+xpO2nRoZy9+`bH7ql+Q zA~gU*)J;C}xQLJ#!Yc8OS_G4cVoyYfXgTrOoJ9O;Phd#iSQSCQ3B%cypnyDd%eU~x z{b*um@h@~1ZZy`w=s7o_Xh@(UM~K!+IHCaPA~jy1YqvgiHXk3ZKN_ZlI>^-@2NI#V zZ37?EEsuc)%@D@ZvcXWqL9{@-i825CsI`0)Qd#M7(upfxT|^as0wl<@z0caMSq z6U}k-NjiuCx2Et^Ex@1Ga)Sr8f*rsm!d*QE>_8m8znQ6X(cffDXRyRDtvxztTpX2=BGqCq%-z)=r ze`NyX4jzaI9Rv}fio6&&{?5SaysK#3b;We_`b7h*uSH|8JInIA*`r~ao?R&CrBp=G z6}krCEPBTcNI(*y?>Sc%7LIuTii9|cu1vb6lM6Jw6U$B+ZAkV%o zm$TJEg^sSP1G}P;Lryb2ciwTm!C$8gIiw7^dImWQ;B|C{6%L`6JuALKh;kZ4<9_e6 znw=`K1$>2GjsC`jx6@8Y6_IUteW1U~bi?QgX67#}flVq@%%^x(OcO_Q-!eK8fDAckRRht{(Wh(jdcN@ZTy&=1 zzMup=GXXjGIsgm@O55NUD2yq2$%#3yhV*N>QxwYbRsBtp4<7n7BoQ8t47D|BHGvl- z%zdi;8wcd;F}3(Z#%sC4X#Am4o|P&UK8Ky}WKkmVza|ge zz0d#!)NgL&HBh=S8i~JEdw3`paD~tk1X^=QO}`Tr@y`Em9#Pe4#E%7Gycs3Oj|q(5 z>6oHJi9}9C>^lIUTe|@Tn(sTd&%2VZ3afz3k~cth^GTy)3gwJxtHx$XKvT<~G6?#t z!}36=Wp+C8qeFMXv#dvcjhEeNAZ1+nHncIXjjUwF>zNawSVjQ>9Wq~BP#*tcnLzkx z!#C)Hz9W-n<_$Hn*@yUKwh5hM{*ip}?pKJal!#{u7iiYU;dtW((EIV+TVyi)*#Ob@ z6}(^*IW&5m+-czZuYCC9!7mVm0V>>OKmr0wUR4ADS8kD55tEQ6r=V3l(Xv~(^B6$I zn*D4J6i@t#pDxBrtNIpCOW?p zkG#i8>kAc!XJB;sam~Me<_$}a#JpwG8|KQqpjtE}A2i6dadf+efQ?AVIlxrg> zt&K7<&wxugJfUj@$n9$mIAi(aHuJJ2%eUm7zMc%_7n?pb!ObO#XQRSr{}{M1(I6;* z2qEGDms%Q+!pHZdq(g}30i56`oA#|8M_iDCAz_3Uoitx>&=eyB_hWsGqm_cQ5EyL< zN`Q$*LKtgz+JXS+N)&GbsGvV+-mDS_#p`JW(52?bygVNot0FXRv0DExK;0uwAV>UY zu18o_(n)pp{<)W0gauENSu7^?h&Rt98*XR{)qu{2T0CGORt;vU6Um`>00nBZoT*ku z9e>WDQX8dmHIU2qc5J)f7dOF)3KXdX0jo-idgg!8aq&WLmXr5W!mkcH986d6guo#+ zG=W88LGZBIWH7DQTaP2`TdiM0dgW_mwAk6f>AiawGx^vASq~1HIzW~NFrQM_!Sf$S zaWTaGj;axUx-_cBl9oU!S$bwd7-xEDzo9%p_j5g*R)V>>x@P$h&{UN5%zOv$P0o4< z%d|ICrN1c0itlb|EDToRk33S+OqK3-4-c32KkxF$xdmPNuF#q07&~o(9`$v(4A%zr zves=0S1o>2INKe2nt~-R!;*Tv zWNXi?pDcStA8g&4KJEQ=d`+5g`M;#WeY0TNnXk7HCnN1Sz;LxSUS-gi$~J|b3@gY+{RK~QtP=kxW&+n3Ro`R(3Y zn`l}29zqet*^LFOU|bT0T$Z%d%T#YXCb_Y!+Y{|Qq~HowzAfl$7T?A%gs$(5Oi6os zk|b)mcDi(E;^Vx0ywqU-IWscqg4+6UU$E-nmY{I$QLXKO`J~JCq-rEHuchr)!?8n( zKK6W|w~b|Uq|DOvS7Y?I)W_)T?^8A2b%b?kzRvX?X=YUuV;&t9?H2}vgNsoz9Fa!3 zi^W$STVoAM?^pBoQXLz_$A8f^x=BCqj$<|%J zIgzLnV3_#~2ph(ZdAYm|TeY@}G0B|U?(iWeJ>V{UNLB!+jeM-Y8BZ2>*GCsAz|K;*6sbp zX*pJz6AG-wVNfnk=fR`FC)3+k;~tBaD=M|qRN8m|V(@4PXbtlSl{u;YBN>h}z<&;SJ%nnw@qf+0fc=sC5qltEqbONHoEpcBZVC*==dIl92mOO!l z>85a?uRuC3c;)_i-}D-PE%bL-1U!xh_fyUt1(c%TH^t$P3l#)TDx1>$7!?}+4deHM zbLDXrU~_-cpCIXSR=$Rap!WmI8KkIhYVZ{sE^cNaRdKz?l+2(S_#i>$3+y-5uLzkN z(SQWgT>5H-U}q^ge!zI7cHXk)w-%$@o=cROe9kHWhZ4v>+1zm3r(dqY$^X^(`{Ek< znE$uw`8CpLal-CDq|u*%c0b3pUCxFM^Bhh8!4h9yj55eNd^wzoFr7AGI#k&N+|J5p zP*Sw=lo>6@7FN}u_!Mk+!`uy-+f832Up)$;o(caeIWzte1U2wOr%Ws8{jwXm7LOBDF8E~?5(BkU|2-n!R zzyIU93wSgbr$fvqG)aH;#Vo2JLjB>YP&LjeUr`y%N7pP;1=}rfa00j^yuWWj_5JU! z*MV@_6YKqTot7%a=G)6f^xVQ>aC#6W9OCv0D6Y?iLCN&(+A5D85@Y~R?9cJjZ{FXw zonLaJc8`OM<7HXh^}0YlQxTPs6FlHak|_BuV+znCXfCTI_Vco~4}b32rMo^-e>;md z$;JK^IACC4I3ABJfuGE22vt7H{^ED(Ff9Ldan&mIX}-d9Vvn5nsjeN+|2o~0E+`~QF1V8Gk6MYeGW=nf+96Fw`yRB=dIil|#UD6_}wgms(x!YjSL@kM! zuq^-ewglE-;QV_;xDQ+--}s)yo1fmLoOa_3EuS6$5`tU;E^A+sL(jP3j6;H@pp2%IQ4)~hMo$NZ%5bTet-2PGBKcUT|!cFoJ zrYNbA^zLTMKf14!5;=h?EfvD0>cWpP%R#U~&aYQc1<9b|2<{x>r0^0VGW#qJms(Oh z-Hj9+?2Yn)M%PM*HXEv$#OsVok9dLEZCVLJOWVP#k0vOMg5kgC$YfuVthNhFOs9VL zf&0@^POj2&@tpL(^JU~m!h^evSH_R8OZ5BxH`07Cjl$ zJie7TBD3|AZowR%)hw;*lln+bQFux1nY=ZLP14;X+kiQ1gkeY-4@Nl_LiX88xm@_Q zaFzLhkRu`Vi=<sN$dA3nRTy)Mk_Ip=|1KZ5Sq;I2K@?dKyN6C zr~>pnfNao<*OQYuYoE<$J%ZU1L~wW@e?;C6qpdX?Y#XdN9jy zIIzgO%|w+nYTBB4o!&irmZHq#Z>*iS(vpX>7W87R^X8zQ-pW#L->+5=jjPR-xtD3Y zaX$gXF;}YCt*7CVjMYA?@WJnUzQ3uB`T^{bdOj9Xa9)7)0NLObOK;xt8ZI_xchY_` zo^siS#L%gP5F-*;-Z?C7kEx?gffH#mYw?Tm8H=ntigK;_{d07$7L6gu@#DU=b-q89 zfztSWZ>#6|3%+5{wUvA?(h0X4p77qD1RlRk;>%Myo(CTpGOpeY^=j2BfsJRK@wMI` zIp;lm`V!se6ul2Ww5DO#QZ0E%b$qaZEOEz5>$|us9^(PXqHNn}1g5_i$i6v*Ys?p8 z$A-m|P)>J}$*eT=$zE(NT3WquD1t%u^H47*lf3f*5K=V$>k`$D%t>0{=`;|~Vd0`N zGVGd<3J2q*ohqd+30QvR&&sWO8N3K7+09XL%mlt1(46y6alSjVGPMssA1$LJzKI_h zmemHyu4#BAhz2sv)nbn`3YZ@W+@#OOr?d{gL%njtB5HaH;6a9CBoj-yKTiEA%b6 zq5s6+D)YeBJHd~f$v1DxoIi|wWrbD-cnW~>)+&vHWGMjEOc8Ry45b)vsxr9-US z+fW5-l0Z^Jzci2gftphiW{18E@pCG_|0wA+{9Tq->XEm2N(7{bgqx*J%itd90F>Ut zwfzeenssUZbKS|xZKLc+#;6;zBPFQE zto?*e2s)54X^={4kIpp}vEK1rV*?LTg3^;3uR6ZtE>%7?FF*tNgwHq>6w zpvSVLIgR+7DGujbVD;}38>BDzg(FrGfxu!b@J>PsU&iJK{zNnpz7v?>uaDyMA~=Q* z9>aW2tb;BpBlzPk8C;2<2K%2+{TnZ4=aq@>|2JPUUHX*y>ZO4mMH<`;7;YLMpaVEA z6?>_!jA8_7f7i*KBP`_HV0)Mcr&#&RMHLR_`)1V2_#&6>AGB=sT6Aq!EY{a-d6S;1 z$@1T?eyD4mamPtcu#`;7nzfD3r(vM%@QbQVn7n4Y~porsHm)srH*MVgh*{?<<`co z8KNlGU$2A|`{Bc@?ewfimv;W02t#IS61P4%#~u7f*82J{dkNo~0yA$^uUx5%{6?zFDL~h2<6yLO_o3RcwAb*dQ z2;NP|NF@wY6>ZY4?B>l(yEkk?LgT z2|otwe{^-HBa)+Qxb^g3p53@jw0r&iACnv+WbDhDwfa9Q zu)rys#izt7Aa4oYIW-fK|MA||o63z)*W-%Q)!U;COsz?~a%^Fw-~BCo+a)RgvwG$m zSq+W2d$z;H6BLdYsbM2u#fjqm6sHSHVrD-mrP2b&cY@TS|8ydv%=S@&~okl<^Lc;2Qtkf02c9CT#Qd<^H#*s3WSVP(=*` zf%N5Ivd~eosD|??V%izNwd2J30we+0G*VJY!eapZLPCc=*vZjo+LZ{^t?}(QnT{3L zuMzIjBb9hDMjq>TUxuk3mqGl`C!_r?&(xuE8mIP3u?~78DMw4xUU~qJ{|c&)$2!?V zR%G}y)+{uIyz;G)-+_VVY5u8&A8xA95*g0=G_K@X=(=QawxPCj8Y02WutI1IUu0cK zBXELKSKp|`zwtCD(GrUs^0xc4QA=C-e2QM^Q)NF@_m32x zp=!FXyTkE#%a=(Vvd?Cw>O5iFTrbKWw%Yog&eg~7W)1)2KEIR`@RQ>xaAqn_-~l;H zUB_?F;{SkvHZQJjzjAWWez`;B^y88nh`(#`zg~(3YiB_$&uEmN+$HRhcx20Fqmg`e z+Z0v^&2TY<5-YeUmqb8SBxy^{GX-IqJ@~>L7_4|cqXDpuIR8Y8B-5n{X5~CZh4a&m z4U5qgv0>I>yLLqVAUN@GzpD3=;yD>dqvqw_vXXF+YS10{73c-zu)3E`Z#l% zUww<&Cpc16l<}GQv%|G?JR?nypS~l9uQxU+b(n7xIeny@DWUcIbU=?3#PEZ%Ax!s2W|GoOQ>I0|)s32OR|H5Ow=eB0ofH;WvkThmOk&Mre3n5giu)pPv z_7upjvblfuS$Xl76+Eo|zeq0I-Rzyfr3%}l*gyS^@-;7J2`e$P#DRe?+f*M7s>ue| zTWt$nsg*Wf#EtcsuTA5C86$v!1Ky&Ew{u}6IWqMSaI+6NUT+%lVxE65E9_>&+})f- z^t_qxbZ#XmJ#M%l`XC&&`U18w^%D&5%NI!xf7^Q6V*rzmzGPvb()@m|uydH8-_}Au z8t&=%PTq-Vf>(I;WOcd_F*4%lP2eQg4-jF*|M=T~`DtIAEmK1ua3T|#*r;LrY`omK zHlk16pF&M3Z!bbxwea3`TOdR*lB%CNJZ1W{U>osL88DM7s}!1<&mD+8j}JPWBma8q z9}+sLbUpQ+Ey#=c{SSKd!?0!ItxkOvpXWXCoiF@h{IoxSp0;Gj+oV50f3aU5nksQB z3bsd@@C?U_?+~hIL{1ZwD};^&u3fdizi`McHHUZmF9(Gt5z@NI{?JG#;)HVd#r@PA zmNMN)hw-JWzamF^Gv4HD((;l3*PVDmFW!; zfschAQq1_e6Nt5!R>2b=TkwK^iu$x5y<~Sa(y{wHKWK{ zRlyG;Zm7#UZ=S_T++)E$DvEv83c|N5mQQvSP^$#PMdsOoh27QDSC=8CM*xIn0<+Ih z6O31&a;K>fx0oVJ!;?^`9%d!he?CtjvK>k{Bk)C`6DOkLoksd1gH0(LCni4H7RXnk z*pw>0M>-cH<$A=);n~U!(2@qY-<_KcfGhc zr zqR_vZ;~$!wql};hHfx@9y{R!k8p4HPp~u`1mPGgy~U^k!*3IQeayBH;**Y#ulR{v@?PKN*D*A zOJFk1muFYJ$pGbze>MKT79!{mbZN}WQ61U{?UaMuWBpS{D|r3lEKR8=+E%u=6mEYF zMsxEyEpRT0AeP3RA`T_jNMF@h37{OdG1Ug)@k^M>2O!eaba;KOs8u8JD2~)0Gm-oO z;i<7pnaVZ9x+|FZT-WI5(rG;FJSgP}86CCzDqRJk>oRy5gZq4QXUWN^%A~Mil??^H2s^x* z<5FT50D#W0FUKWQUAE*u4~>N*`&hih^iTzFb#-79ZTKSoED3(^z)3aDz)n5G{%b3H z*}Tk4j5S?@yLYIQeTzDNFwLKxCy0ZAQyu{yQB@SycW~JM`xX>I~Ud%!Mia0fmW43U+->IKkA&5)Z zXOW)Ss@q;jsR%~DC!KfehKFw*4x8H!-#0$tFtluoe8P$|M9`S)IX-A$?zY=XM;A}) z)WQSZQhgrt6S9vTEC@s6e;HG{UFcU_i~r?}5`4@MoB_QbA)i0r_p`{(R(XVq0#n>$ z)tHs0FCKepJ}sB&Urcj%Px5T9PqoeHA^)vgwP$Murw$MQoAqSrR%&Q5-CEk5ZHxVl z;gU4piu%lm?K`o+uCH;0*zDvy8PoN_{pasH^=qfU<9nuT%~cL`D@Fb0j@gHC1@5fI zN(yaX0;_sT@O8NSM}W7p&PPpQMnkVNF~+=}kK6U<0dnnE5qX^1-B5Z0Ey-fO4L>YnIh zb|ln@iJUeb2vvEuE?1IEwoc-PN;HO>-o#ACL3HAd5edJ87RFl&lK!0E!H@(6Uyd@d zSNdIg_npKxK>8EfVlYeRhwi|+oaV9>r2rsel6%SyosG;*A?uT5HgmDgOQ8o3?s=QJ z=TRf4N{*OvwW~?~kh`ElF(ZQLbB`bj$)4#ni30uEG;Vuyc&1JHdM3TQrJtIEw#;dQ7RWh|N{5tF@>_g@X`1gbmAoys z{$}t}_!|w2Q>UP0KMOxbhQN-6igB8!9RzIOEYiN9pD}hk+!9gtK3RIWfGn$0%=|cf z_M0!68X1Tf^f9g&#re_Kd>~s^@@ZQl_9LXtug>@FDq61hXj+&2)SmFZ2UoLWf7#a9 zgXDwTm7HumoqkksDG%WI+I*S}Ki{l2W~F#|7{8J}ZeTrj=wZWI3Jj)xRaY=GWw1 zp7Z9k`zA(=_~suk?=6Co46fKT!=Tlw>cdLLD(b?}r-5{AKd5GwT%kEWI;O^6SmbcAL&Ii{ z{rhz{+^2cDUaF&)LEA4ovmg7d+eM&w^I>gt;-2ZZZD+;__HJ+W z>AA_FKMN97&pxCm1(Q;s87}ef_$Gd&eKn!fgD1nM*YjVyZl1k%chU$ zkh8VHkuBKKXo=UR7j0dTX}{17ctY?E7KbcIkMT+C&*55hrMdH>u-w^#si@!G!}Oc^-RR5>_z1};#C$9$H7wpsE4k1iD8AxweVS+^{tqxzfmcDgtN7)X zj0L%BAtY&adI?9b!E4?4&BjP_6$Eds6Lny;mvptM3yXMw5xc9v1=p8-{E+yc`MZHE zoXiC=bx2qe>3=H_wH=Wjv+J3e1kV-je*op4$%dbxdm?jw>Z3IBY1^ln9(__sQLiEN z>Q3H0dTBQyoy3wNzRp$06BPwD*fMN_ndmpP`2P_DOHl4D;ZEWq>ps!dM*nbNBn7)P zrK<>u>;2I6+R8h1TOTK~IAlq!PEg@HLF+n4!sQ@Ia4G0g}=*)-v|cL@^0(JXfj>WqQ=3MCKW;`GisCaTI7%E8DTWGAiO)@Nu%dj0bY6@cHsHBl5X zR86ooUJ}Ge=)x_21yXRuKsufHGI-mVu)1 zvo*PcI%J=#m+N5dGxweoXEHCx1Z>U&Pe-5CqgDaTxJTgu>l*+?iD%P>rpj1|p=UPl z>EkhT-gmvZV#%-)j9RU&l&^Y*B0%@PphdzuzH;#3MZ%@p0~*5Y_}v~iaor^lp^$O{ zdf#URBL281nOpLs)ozIKl1t(pu;Fom|Gh`RWYK1$auaxd?BG)7c#ptCStlIxlKHO! zA^t-adU(Q0At~VgzWa#25q<|}_8!rN6Iq`;Oji*{y6*wTWe)0m8#XbK)d(L}hyN`K zuY@d1&|^QwV2qQA#S$c7AL$wEaisCx(})$0|3>DXD?JO5;@XfT)hj+-yPVa#6n}g) znf~b^>NprCV|Q|S5CnpC(cO&;+%GVGMVv;-`Lv5tpMR zU}fj#dI%$q9>kd|)~1g7hOy&xB`<=8EDBZdv;FfFMQIn@eiKj5mnVQA zM4g4llQKaKWKpNw$|4l8P!a)wo(k|DnEYH2L>VPwt-v>ikFU)!lmakln7V(E_;n7R znHSua(}@sIW1WDUe-F~>EpSss>a}}cuTEpb%tm?77C0IO5w;fZq$;m{f#9@Yh3>dWX~+d*S@-J^CEL44p@2A+a=aZ!&QtkE|l^f;!zYB$u$&U!So}6 zg6P5Rk2lsL`Cs*%D)@al$+KdiIRfF`EqO8oJ{WEm&HIFE8fu@ae54m|UawlvepT)! zhVKgO1j$(|z^#?wBPxv0{fF;}`5vI?ol1#OV{Xx~IvFf;Q7u9DhaP| z&4l-3%gnICTYPcl-OXQ>L;uLS-~qV2_=AJsj=E58X$bsKJSP+FU3%GM!6l=usT zbYm|+t0t4Gfw~t3jGvj86W2zlu`de}wg6X0E=n*e%rWVo!F-J2(GhL890+F`zJU1! z73RfYi(Y#GNi8^!N3Qn3CNV%TX;#%txYOA)-6I@SAI}=cij#VIYCAPfTmO?`|2K~b zZ0va=MTJP(1l-f0Q=;)=(8d2$XJ9MC5ntIO&><&air1ML)H69THl)OQoETxruOK}= zGsk-P;fx6)VdXKD`v*C4uDX>$7`4{vdiD?rn+tDw&qAz3h_A`=ee1q8el?2(x#i<- z$op-!NUNZbb2c2<4jzg114bx99p^70l&cf+$Bd3v>Pa5Pcm~JOoIBm3g2K9Dq*V5W zImY2L$p}}u!huIF4h7GoA3<(fV)_V2zOqN}oD~W;<=3>>@#=8F__N714|}rT-0xH2 zjb^Nm+hIOeh~-@xR&QSZV&MYa{f2|=$v;L#kk0e9b*y zU^`|DBNPDAN^H*#Ra*peJN>s@A7&Y4<0^;Ad=J{E`VSimTA3c*d;fBc3cUnv*zI`^ zo$5EDpVCn(avJhTc>$H%zVM&33gPy<`}u1wF2y4p(VkJqK!{RA>`elZcY0*_wQ^5H zA~S#d?NO}wqQK_iYznoQ`n3Y-z1}La#)K_?a`k?_nMHqVF2|pMziJdhxm@~>Zf2Y# zQbM^i_2Ltf&qfKg7oj2KJUzNrwKwws@*Dm?eF>$SJ%oN zQTEafC!|LcWYQ9J4g3O84~yG->&g3rS-F!+)1Ruh;Idjc4HP2GBS)`?d&;mHrA{X& zG(3Dt%+dsQ1^N4Codk(+QeZ*<{t!t!lguO~4Cpa4F=7PCYnV@8KT-W4PjnhB`^9Yd z>Ul&by)_<4fF$HlxGBcQXpd*YOHwKTClEn%E$t%IdquJl{K&?@uc>+;By7C!x3)ke+Jvy(X8D5}I)|Aek z${Ts?sBxYOUZy`|?JBhto30Beu>!yY+_RA=x!niaKkNpFW2p>LHrinEA64k<TIQjMdbnaZXL29OKj||qZpw^~U{K8ttmJ8T8@4ShveJ3}4 z6zCYBXfAiML*6L-JKjZpW;zAoxzp=?P&Hj$m!14Zx3g*OVgG>b);LzW!$1IYu=p#A z_?HZ=ig<_?+^dITDgD0#2udR>am-?&pM;dh zj4Dx7sYjTSG=$S<-x-n)m^H*6FEJO!P>#bO@BYyawWYyybExU$sXLN`O~A&*~;M(ie+Xr$we-ZOW%dF_F*-Uxt~<6Nr!`f+}3q zu=tl<{LZTB;ixTC#@E@p&VuohUDaWn6 zC<#0QO&TQZuza*SoqMtj3hKFBy}mkM6sn9?e`=&3b}ZwvY#X9ZBUQir z2GBD3?2y29&Ox#nf&}_ebHioJLSCJ<9CGp(e)1(R&M}oIWWN(6f316>gQbk zs`-6FkrMx*&Wr}l=36hX=6Y66Ril`p^sg?m;%%@=j%ZLa!c&7*(lyOta}8MS7J9-B ze7a#9B|fh-)q-|%0#NZ)fDz*E^^DpwA5YmNjh9lD{TcT%o2~|1SY3KC>c%#E4EkF; z^o&hP6!v#&!Q%RWoo3QMhWTj2-$%5mTQjgVe0B$*N;7>FwoXs75TNsK+i63SZ?U== zZSt+WIlI@IU0R59K4rT* zF#P#4yELTVf$~Jx6yKLm8rYpOpaeF;nE2ZeIa-_*69<%1>KjZ+43QH9&dkR@Fwbw} zMAlzMTxHXnEIs}ZyVe$l)=IdZ;u|vsW=6xPGcUj^MUR5;mGWbxtftmhLb&`d)Z7O~ z2Ct{WfrCb7HG^jG^~b!Jxi9E1zIcq?caK%iVj2MIu$&fYwj)rL8gHtO{Y_9>bFOrb zU#iBLtj(MIf*%tF+;dduQ39tyP?V&T5)FB->Gn9CgroRoSj!W&%Cfs5{Sakmx&fLS z^IW%2?8VFcVBPj5OHu41C|INuIM)Aq4y`5sFRu)9k(CNK8 z6-C}pd1i)a_=$}4<#K%FAISQd702(W-Mju+*fUCeoFP%N{F1VXD_yYop`s3z@+Kui zLGbI*p36n;gT0o;lAFB`1&VbP{reTt;F{W%zQ)e4iA30Ki*u+-0=ruSw7VY8Jo41I znc2V-5GsThiDQ@50P*M`UU^2$=J^)(VKfgFpGfEQ4>nU_g;!s*i4apkh#8StuZ>XL z#+ekf&3r_XBHR4seY!~&go_s_>v^r&i&^gHMc}LRKYx9@#NThZp~@~U64kub;aA*` zc29qzJp6Wx)`jurolA|9;Ddw@ci1t9Pf?d#yW2#HK$+voUW?|3(-DE~WN2A*Kmj|5B{^ zFQv%86Gy1$oE>(4i{Qc2e~9+yCJ^dr0CtPwR!DH=q#PyI8E4(^e{0N2EJFO>8l&>6 z94kDr5j4|Q-(Z=7ES@&ox9$A73qX&nhxAK4l#;rB;FmGpPPL8k+#g`nUB13NxRB+V zWcBKHmCVXBuYT`2oNbM}jhx;a(MIGsD5Q?dH@sOlE94X0Lc;91*2!qf) zoR!bVVUjuB#c2AG=F&M@w{>fWU?*>GM)-L5YbsZpxJe-Va^#-C%SmkU_)t7xr%3Z*Sdl^z%J@C z$HjC@xeENa`)6%J#X(e{>HJl;>6Gc*9*+WAQ#x#_D8%kMbb(?4^auP?xi(cqz7L$p`7JRKPiE*u?~2tB2wJm@diyYGnWyOcz5mM)*{kTQ1Ic>xo&_$xs|qmn#Q3PCo>7s9;`!9L}HaEm%%H62xQ7H{Q5 z`^k6r+GJCYe6Hcz0A*riGeFhRW|#1Mc}PuFZD4Za<;@%9RJx@4uTJ0?8Nto!s}bhb zC;7Ke-r6s#l`sbe)SceFee7g-?iiWX-FF!0=Yx>(Z8ALLyb0}4A`sGsy~{s1jC>cQ z?*D=sn*x^_1~G8;R$lB*`rDfrwd=RR^UavM1g`%sH+Hp@{X)G-kjX1EtbuCykuYtA zTk|;U-obS8{sw3(moTLnkn$DhB;Ul4P2FUj{>6_Gcy5XKjNZuMVR&h*cD=WbaCfo- zDBhw#9xp4#|9WqMJ|u^kr1Um@RT_sh_p5i8oMp2%@Thm)ugoCyH&3mI#^fpS3-rgF zVH%hi!rkEZuPxMiXx`_kU)-U^8mMnBIwt`X*!3sOwSq-Cj?8i*-t*5|-N%k2+Z#uM z-=%A$q8pm@ki=SK^;&i{p~d^#!RCHmfX#uo#!WFS38Hm~DUu{?F}}=RlDbHHKe6L(IW|QO@i4=e(=V#{HZk&GpM&1>lGMu@8+Yz=iu#8CIDhAl_>Vq{A(58aYQIhy zg61MSy|GMU6?07^1VW=Ps{B^Mjttd0s?Lx*-!k%&be;Z%JVI*&OL^D3Niws3`n8LW=~!ZbrakC9aTj_ZDGZ>L-LS zo+CY;r!P<%gZrJ#Aux}Q5owj462{6Oa$N38&Xm`RiwIJDA_{Y)hX3eh{lCSX=ELMT zlGyt~JJ>>{dm*O)O4gp3#El{5zc!h_4Vud#0pP1|8S#>zJ^iTn^FF&w)8<#6IL@QF zduN@Gx`z_5@3~%mrL6+i8n)GarzoeLHDr$cKgFk9dz_zCL&@_5%}2`l0caR7RmaRC&5%C z!39G5T1e;?>r*yEaiM2YVHzO{@ZJ|lw@M0+lx3>$`k>ac z)F+4UFR3w)e&O8JOaunhLv;-5c^8`jM6u!ij7uU!k>k1ynozaY#zvvq0V<%XmZ6l0 zj~>v75*lc(fD6=%jk25j!4ur@Db|UH|7`$6au^=5{JGbn33PN04g+T!7mBm)s;Z4o z#f9&2(_VwTx4Z`acF2NS5aX^GF4E^CF9!nFWX&R&$hUZv2o88Te8o0HibF2()6AUCn)4{}ck$B@5vKytz3@A#b|}0zJ(5DNA1CqwH<@!6 z`BO-D`O^1n1eb~Qr>O$8-7mMh+##)u25=h+D9JcEI^q|R56T(sl|_Z+`~}N=)rM}& zXGcFMM6X85kAIGM&p(n3B39MN8|1cK=gTs6g)gr@|B&H{CRe~$uEN8WhkE~NcRHN8 zccJd{lf4EgGrT@L_~|kMBhtAI9=%b|>7(4g1!)^YoySjqpCb+EAF>Ln`P^6P(Y&Jg z|5wagaikY4*mX!4`vxMsI?-ZaVtw_K6%LG&87wm#9^-Z&o*%`bhkn91_4>Kccl#a#pV_b=&n!i%LK6AN#MEnBeQ0_ zQ|TM{r<{Xmm@8GZwHC?EIz(vWr*L03Iq`cjqpalTZsdO!SW}9 z6*^)B6DFL4NSgr9p#O^ps=o*)s%qV%3{I)gGt9ieBQFtv(Y-0}C`pn~cMf%tE(|d* zcf#QBi09zn%Bf}>c~pJFme3>9Bues@hnd0|FoWqF`XdIoLc_&Kc;+gqyfv>*<>~q# zpfA3^F+{hk0zG6zQ=yOr(S0Mtv*tGB)MxeA@97>gQJIIy^ZvK8PO2oIgdtrspI;zf zdXYw*Mih;!pcN(<$97-=VhE?#&>rD8@_wA$#^}v=lVGqs-#gx6yi6$;_X_a2*XrIa zofxVil{s6H@e2Mrl<;v=-yWJ}{4^_wlq1I8Y~*i%C8DX?ns*_`XYA!@L%AXgnSAz} zCTAR(NAPwkp}?6#FJ&93RyN|0SLbQ2q#wo9#B2^tE#&tiX0!wwPt#>^^NR{VH1CR7 zDoH!*z`2PE$%X#VPy17(<*cF5KIAV+qht8UtVPJY0d_>_{w-D;HU+(ZYGkPUtDji! z_Ab4!t${1P8tVVBj6pO-LJE{25#zb&hI%+E`8|!#&uv8JJP9%*7MhWcE24!I!8(Qr zBoXQyy>#a79{>+PJ|_E4rsm^xJOWurSUAJOX;h~Yh`tijJN9=k)a4O1X;lceuN=Hl z#srkTtOfS0{lA<#U&f@;N!@uXnb9eB{T}8HX(+%tMy*kSUq*WAF`}CL{Tle9!!6D8l}IK19^ZzQg$UW;8Hp1kIjf)FucCYJip7KQt=vG zXjnR;_o1k%STuAR`%`c+_J9mt2J!`FK01lJ_*Jwv(~=kO_yD{5CTo9vBt#M0BfbIE znc#lap+lE~FM&5q5j9IdJHS@qFW)aFA|dUtTV)i|Vv*oXVW&=^6`mGne5GN~Ot@Q4 z_VNxh*{~06J=r4)Lb-b%e!kF9=2uOP-H1s3+lE=EOmjDFw7p0ybeWkH-isYQ%>6tt zd-?VqTe7a81+Z%v(ul+vn6QT(az`0`wChOXdcCSYHdUj$bBCYVFBIYzC+{J;_L6?B z6umy*Fg$a+%HRCULHpH3A~WswY%PBYm4RNs(;F==yK{3b7mcqnJny634Up}&!OE5M zoo#h}%CY`2wH{bi^fF@_Z@qa(Ej5-7SM=yDi;UMxxjM2eIBjxyVR!FqrW-^?G{S9N zRmJ4XOKvgrvKI2O=+ViMjwFm?LgojdPQKkhn-zHY zbED%;LSbH-u8yw9A?Z5$&i++?-_gPLWb1EhnM0Oo_s2B%DY2NoPO0ye=Gg-Rn{I2W z4heQogvf8?)keln({UJEB1h^=JM$DX6WN+gU_1K zub%!tT~zpjxL(vPj0Xb`!B24S+R{h4P6YiU!}bT^4};(HNeC(w-X<3ST(Aw~W+UMW zLDVKQ*BN^$*ou}B1{tPkmvGkCchE1mC{!y8-0@sJyMa!P zqZ{n_jr}{t{_cyZhX?1@TTEVZ0Yu{KHmaYg2@TM~bBL)W`Wtm}EZXSJ4AI@w-;G6b zH?~FoZ>M`aMb{$#+v&xIVP0SvSXIL^0;~lA3;ynb1cA_-REX^&#&U4P<1HlBWkJN_ zxP3q`u)K3$rEi&r@d;80!kfqy!du!%%n-Ej>IWgJO4}Q&0j_W|@~C52^#k~5A7}ts z{( z8oZ9)OF1z6pn6V;9b`5k{CgHjKMl>#Fk$SXjwTLsT#s@nasJo5<0hKnkj`Oat3lFN zHQNw-C6raqk+pv8@!H6#EoN^^U6(%_Iv!ob@5G*Px z@f<wPgXPGF{6-ljG!{PE!gDgs5RQ2dylQMChP}u_v zonzE{M3%TU<3$;D>F;h73%+Y692e~6cf6V?Z7OrtpLVPlkY;?Lgd+|iwj@ikn^taW zK+&^;ktK5)zvFhpQsj=)>5A~MwELXjEezu2U&q|F>{5t!uscCJ`hZ~q@rV34dib4- zR-wy2G7U;A*@cD%k%myd2o32)p?kyRyPu#GhnUx>npsa#Bf9(zQn+){OgyX=ZT%UP zZ}SaIG{atSAG#aok9wfu1jwW5e5GIgN}tzFPtU3ptOw z@=165WffS`ucdChctjM%9UzYbx*23)>C-mfXHA!5JseTm)rT_yD1Q*`^01A8eO&!E z;M4XctcrR2$^QmuUMWN+E9Hv4>blE-TWtulFt&h8;CPZo9Yt_O>V`phsd(sd>^VcK zI2PMdqd{5awYD{`T}JnFQ}pjJ0{fV~|9#@0r*^)8sSQ`*gaf|~aZ|kETv!f*!5a&s zb$?>rNT*Zl96z@n<{H+T)tp(mM0}sB`9k12_orUrO!d>fdv@WWCG8ulQbDske#F|Z zn`Mly2GhJqFBBjbDy%Jx9?Hb~1{jYQGyhEaG4yO*OG`)EKgOJOkQ2bPRe;QPPzPwc@zNBb~Pqhw_ZDZwiO)nT}q z$76`FbZm(Ec>#nN0~zANv3o&K8NG_|h%_Nd3_H)F|*ZM{21EeSaXL`MS=lS1M_1tqkeo zeTHB4#GK0(8dqz6A55fICCk*v2#rcsK(n<;oNXIj@!7vHxQADDKP1m0L5~6tvtBTVmkX$7-+CdUv;}# zno?OVPJUh0*ep-hBm`dm7&JF?N6I~%@%?=<-t$4weVY(Wo2*wPBq^X_IvyN&-!oXH-*hjmP6S%b86P_J1>figKE9T?;9@|lT4gT_`U78 zE;s#gz7^jKzo+{4X+`Jx*Wl%yLX}A;qk;!fK+?`D7S-)+)M;N$zwr_>oPIP~S<#+B znzP;Z@5=Y<{$zj95!3w7*5P`aog23MYrcE)6VG2@D^~*pV!hAfi#tvvuh`Q2zJGA( zun7L}Z}@7jEZ(O7%`xUpufoyA=%pFZ*{Ba(wuGs8AFTT`1rs{S_?eB>1arskkI!eY zW^Z)I+v1RSflJYJuOw1s_sw;E4ydpU0wn?l1H3b0xq2=YTd$k@m+;dx8J)QF#@!f) z$}>(me5c>$uJiANxxNrR)ir+cOE>jjzW4k+*MWw5Ep$Cxe}^X-*OA|Vr4^BJC5Md& zO4e1@H!aK{^PP+ll;QnnG8FgMp|Uk1d!r%5BgN}qqbuV|!SPTd?&^>B6B&n*roVq z8NP-)=Xu0*%J3fkf4()h=kUV?OzC3h>~pICd(b+R)${Chk>0;jlWk^32y? zO5m@1Ks!i7Z}&oK{;RtIRs4@w-f~FlQ-Qa(xZm>QC3gO;YS#fY0+U zw@do2jq1JF(sIbo^SL|*sK^@*6Nj(}9-(rtZ~BxcNJoKVig%vh_jA?patQI2`oc&) zglO@)na&+ti9;{Ita0$;cHbKjxBuzRg>I-FXz5+BypqwZFm6YPkhngnz$i#NzLje9;>D&i)I zblr+|qdOg5b6+2!i#4;K#gWbUvkb2E`>#x0x(~}tZpAj#e%qL+E1s8h1q1IU-x_~t zU}%p3DI)G0Qyo)dXy3S?sy}n^l1@Wta%oCB?)i2Y>3l3Tc4c{?cAkP@_l*^=GNAlX|hn3x@o9AGc`z!5sHUb31YHX`_9rDt03P4NYVQ=>V|Q|#H-y1nnd zpe^^Dc71BNs%~dr5Z@KZIQoC$)JQ7FGd086P2^xIgF@7~;n;^p!f^BluF^_toRX~Z!?`-*}rjgzdF zTHuwU(h(ZvV)p_~e_TLNwf{z5_wnxbk$>G~x_C24&=w5miPP-ta^J5ax_tNV{5Cbi z-TYjgaSNXayC1gb9ACm;VRynVj@TX=tXqocI>?ZbaybyUsg zz4};yEBP_fMb_YA$vnG2Mq5rSW#bH+yP+esIrE~*=C8;-E}&y%3jSFdpTQ8YyU3ldT)P|D!rk0*Wa4q5Ei=8jroTCSdrPf$y*1<71do<=t=_!7qV zC`No}_CBrgT_8FAi+$5;?-E-qbAh{Gv}ep0GGF@pW#!KN#V4SH-NVRvciHYg1=kfX z0!e}L!#V9Gl}lIquryLFQ+dAaJDvYe1#TT@(Y=$eQvT<3z2`y7s_ccIj`Zo;xVKYJ zy7U3DUzQ4se;RD$jf>Ig;ij^A9P>YHM_8-<@Oij-4QVZe<)SD#Q;%u4KWxL@`pVa} zD^G$i8>D2UZ$_9O(T2;OADA4Z;`!}ka@+gjc7Ch$N#A|dQJ~_maT|QejIw4A6lqZN z!Mm=6gn0-!O0hz01k{B)W0nU-G zy{{TYof7vX&0-#|2(=;nE_&#%7S6bevgAt_&Ku;fQw6Qo@FN zie_%Y8hkk{J!pkuh2!HAxJ3Xv?ewRZUT0WOD|YM|W=>szT+wFth8cX(qHWcP}>x^|*{LZoj8c;v!6(DjoVSzg9NLcv`TmqGq{_3j3>c zb&qE-xK@h{l?p zE>?kl^C7R;x$lT+Tehg5-Q4Quu^{!^lY3(l(>!?Pb4>|ZeFQ?kI9vVVf-1h~Jnv|H z^+JCkXtMpzp0eK>@FAMF+RGw^Us-fq6yXV}R({@a?#HJ=MSX?zIMRNizMIWXwgn0Y zkKhUg6d{&$1@VZFpm1QmDgtUoh%pDZ!#az8n7Q1+|>_*$<&K!@quFeVU zEM@S$*v~(G@7T6yjC5+>BAXn~@=BQGdDTAM6Ss?z4Zn9_bMY^drQ`LGk%f`mJ^^(9 zX7r}d2=~1a7wF`o6Mhup_&zUYrIauwu!l43$m2=&U(*53qUz>dmEY%rcb^?Dp5c^+ zN*$EzJNe$knzsJb8urfK$yn#ZVoSBCy|GJ)<8KSp8 zF;Jk)UUkD}zdsK8CUIr=EbYqY)@A0;0^nvFo#VHbrUNp%`R*jlIj9+3RO;s|@8fZu~B4%%wn8oIh}{;NfF@nUlE>jp*b z6Qyn|Fl52kOKf|DEs;uJO^^`$aU@N-o!R2?!~mxiikbEIa5>P3iopL zcIgZCrM^Q4Hr_a{kahKC%wx3g=}`m&PTCaHufOE%k)>9lYCzf8zBmbA`9@uuGG_k_ z%C+?)m;ap2;#A;r?25>hNV>i>KHfw5-D@h8x5kP@nIb#Oqe|)HOrfdox1e~>XF`6< z=_c&tM+R<=S84W#Z1by_3=GE%Zm0(=Y^7qUC2jEc7fAIcQyzaQ>}Y$wO=0*0J{yIq z4nvaMj5?tsA%4bz84LR2X+K*qTICrmL;5TxHAq=kV0J#pmVh)EUp;S4hXUL4L$_@@ zJ&g|Ebs$7iGLiYC6VJEDX!bK@=ql3syw*o}bjd(5w_`f$Cxg>%%oZOWZPDn1N~bTc z1P##=i)py~HZ)E4&{gy^<|xs6Kj%Ih8dU3FEfv1B<9eS(&8}#Y_Q}x2k;KRgicg+a zdt0ewZ|L&wLhj6j?>-5-aF4{Vs%wTUAQUK;p+8@9g91+Ewb)$}b2S8%4u(*HO4HlJ zZ9MUdk)Rdc9~0CmBu?BH%B52Hmf`EXxy9r z*5eO<(H$w&b?9y1+X64aBf%@b8?<`U_n}3CA0DOXmNOjnx`Qd@7oF#WQfXg#359-` zoB6`r7VR7E#VItN-U4ZvMG z*Uf_ug9qc?Xj`Jlu^*?MM)$5NC5-q#Jcb1rm}~!i#T)E=nB~^{OaFYPEPmb+DsXEd z?XUlvqlZwghrFVuz*jg(zLC@}mt80eXJp5K7lz~a?1C|v#xBd`N&KJ4dQz3(c>PrF zQRlWf?b|(<)2DxQv*`1V!lTj-*dhx_beL&Ac@0zwZp>@;@p5Q21@9{i=Zy+x;BhLsv8X zVujyH-^?@og2+{O_O8u5k3xlM=k2e3w{_K_X4Q9J=reS9N@gFfKAXO}#NCk(u!S~M zysN8q)1Uipp9Bo4-?Fb}13KO}0F(P{z3`%swrK&R(xg2z&UficpY+VA{;Q%3mO($# zXKHYpAIjXg^Rl>Y>wCwsDpu3R^HVfgv~NzmepW{E_L~n~ye6pmbMZVM_^4jrswwDu zW7!FKDU;?Rleu_e_MKqp(OW3ta>+KCXYnLtn3e{*j{{-|M_j1nRZc$h5;p zPp8I>loc!wQ+GyFW&BLqian(`W4jcul*~!mmbuzbHOf`|*OQCwhLg187sKs=_`F)J z(%eDjsnm<_7j?hKm}z&MZ+R2WGSh$J#dc(#LAh~huxAISO*4O*-1OCN{3=`x(@iS+ zKs(#G;0aXb!Aup$OZnJ#jfz8cpE2I3Eiqbvf7jW}wube&{(-)k2j+uYyI&O0g}DXG z-&5b%P+9EXmXaT099xA2P$JY`Lq+~;-M!aFJ9w9aCa8g|Ik>vs$*yhzj9qWXN8mfF zfByU7*+{MSycLVzFz7w&cu-=$G7Ts{K}RIMbnqHsM208z#usHvd3_(cJjmuOB9&ht zlQ*Y``hzR;1l~?7#5Au5gfhMoBKolbh~orqOdHnFS;o(+IM@n#e1QM_efjKG*Ha4x zbhHvwlq9nfqC(QRIM?WFH1R1f@Yeviop>GNr&Gov`J0jN^%19!TsxJbrwe(qa2 z{I){ZA0vxler8^`SQM>X|8-0Q+saR)IXz4&5#2t|< z_$Bf{CG_*JO2p#O?S7(l(7u;-^&ypE*m>Q4bH9t-{|D3%uWBxd2jlljoDeFT5Dw8U zxX?qdqc|~C&ph+^Z_SAQ4Y82Bu-BH1Xkc(&3kH3U2{6PP%pOsJ%AO8&xnJ$LZ-2A* z>#Cg%I(>3LTfzY%>DUX(K7UwJEaczcbp*5lZRa`q0WCHFpa0`?ZbO+%Z){veEGt-3 zv!EI%_2z=2rU>8UKwaL*nV^WCMP}ab3M*H7DcB&jL*LJ0ua;AOIFKI?s&QTeDGm0b%K$R`Diex&Ly|XMr?h(dOgK&dbEX(*t*1M!EOL)zBw@d2XxN zpZzEy^Snxsd)&fXYNUNjeHh2(VT1WieIN}<<^bS;Hni)ULCb`k@y|ORuMUP0s+L4K z>Cx9UJ)-N!R$H!hJ~b+@!#7O&iR*pj>Q>VJwEh7QzpDdEQ{-dkh^Gdk@yN87hIKf0 zylPE0xcp;dQ*GgupYDNDtK$U8IBt#$UXGxe$x&F=hmd)~CTAB!(+`5(2`cD(Iv?h# zVms%^*&xN*t7%W;nAFU`!{E-{ynp&-7cZW4Syni!spmA(2rh@|S6n=I@1m&4wZ!-E zY=QWz=Y*+YxlET1AQ)O{k8TM213YdFFq;M9v&S+8(XYmEjs&O|Tt8igB9vzy>NzYY ztLOYk1#qc>$$xteVtluG4D)f-UGVEXP^*|XIP_t@v%eIKt`w$YM_#sbJ_e&wriK}% zTiz99kQg-kK>=Uy!RnHM5tw!QBv?~luWTn)iM*ig1@F&WzLlaDz9+njzQ05zbNr78 z#T?CaA9sZRCAMa&-jZ0RuoGq|OOaR6f60}{Ir7{{+#ece2xBs(;gy1}%7DKw{u6~= zAedm%ACkVjz(jxNF-vn0wO~k`?4Nf~8q}!(!E6&k(z2)D!eg+7IG_@hx-2XZvI-aI zXgZq+)lAkctcx6Ev&I*ZQ|fc!ooZ|U_?i^GT;5Fwov%u{WDy9l@xd2SzuWW#;-gQ? z&Eyj%z0#T)HExREn0H>SDP!Idd8n>Bp!w;orR6c6r^L~y74vNHqq(!AezEe(OU=N? zR3BRaFzNwbl~^dk2UZrr57=LZevhoGtGW)dF{61%k~?Otu|>SUjx{UTdW=Nh~8sY zeo}hQTFW12=Ze3FvLlA3yFK7*%&;{r*eHIk`|}()ylAE|A98iv7JQesYg)heNb#}$ z7fBP~elx8Mc9}Bn`QbE|a?;tfWPSp<4JM13FKjLh@xO_CTDefO`@-F|4KgFc*S>nw zpz*3pa<|xgut27ueg)Ik{~KU~X5yihbE?BvwJ|N`RnFr#PhVUcnxKkajZa6gdxhTQ zeC!y6pyDfmP+gbGWKLw0{*J?8o%3%UC-*LbjvVDzknz0$IgXkIek8ANmoSs|hOt?F z`W1sjPRtZ~Y?u*c425_rHrgjSTz+hjc~KYk1|F*L=* z^xOo2e-P&AlBjejwXwKZr-CGWFDf3sTUaH~T83+K`N{(NXYTju1iV#zfr`on`|ep4 zgt9xL)Y_;TxI!=@j!m>xJ<3%E=pVW@27cp6aquTgeLWFSQG*4rY5^&GsORqDDCt<% z8iX5D@0t8N%b(1Z5QC}WAk9xDQt=@u1L+H8?6u5<~dpHnN(Q$fPl2sYKPP=%< zMsD4;N1g=sy%`A?V z+?mW$j5FWZizL^>f$Rrv!8xy;&()RB%4%EgZB?w&|5^SZ$hIXmvu=Kq40RDX?P7e$ z0=LAa+t_kNPIRam)lJk%wNxofdUa{JfT|CAg9b8n-2;jJ_qkB8YM9 zyn#&P4sxePv9(ixVMm1>zEgs4;vr(c1uMapbHw+NwrCRG5rA*=FJEBIY35j@XAyb0`u@f$&ga6JRLj~!AsMJ&6vXeX zyvO~uZ2Gls^!kYe!LF!)NVR$nOQ0p7-1#l9n}0Nqgse|^e zf7`=YCeV(uMe`;h(;m9>18w)tYtfHb;1zNPOdqrApE_)eI6+sD(9wkMU<2JSO`rPc zg{wuJK)@5VY1jH9P39C(kADl(mL1X3(n~>Kb)w$--up8C#E1EbuLZT^p_MBC>0bt$ z$|+{weFggI6L#&oH%ARyl?;*rWWl$@F5SiPgsh`Fr7ub(?z?!MPD%=y6U2x%aSL^O z$t9RPxe8zX|;;FH_=y%SSGKCe4NCQ(UNq`utpEOmhd_=fw!9ZCE z+ILhu;h<7oJd0zGl({V{H2d(x6HxM3@v7Gi&8dC=)0{7BzYg!wfDMwa$JlT#;HYVn z`|p|J`statXU;o!{NkGKh7Atn4#7uzkJJ)jgJS^4e=BXz^5Y;MGI1-GzY96n5O06wp!HODq9(9qzJ< z7wXkf(MIUhThIoF`az$IDN53DQUWkrHV9uEebD!z00IU%1Emihm^C#&v=NA*FJ9BA99MHBiU7T0+%6Io{6y7{NC zPNL|y5(RxHIgmW&UP$QwIu-1gm&X_0A^<{fVj!xY#V1}YyDH;jIkX zQr+WuDUUw6+JnuO5o!WclYYfYwE47i+eyR^hK>f^vXX$$X!BCzxseAOXUFCbwx{Zo zCsp+)Dk6u0JgxFAuH4>tMbB2klH;+i8E?lypu2SYoFPa2I3~xZbv=+Nx-u@jq(T zlKUudtNBXxqG{5+Dmc4{Z5!YOoaXf%A{XH*^eh2(X~*qBcQ^BIh#0ZRbQB+;WCZu} zG#*VFJb<4&xU_L(Z>-z0IQ;BP>JIS)R^tt(&-u)cg(K_cUd+V4Bh)iKvp$Z%cyO50 zOGfdXHB3@ntTw`rZ>R=l`~Hb;FPAzy+${W!`%xUO`-5i2>Q9>kHpR)I5JIV2&)Iph zO?-$hM{d3V;4a0zxcg>9aW4|hfz|Gq~nGgTNU2~1i zvf6LYr^n^1K~L}pmhEUyY;2r_(Z@k&PmgxQI&ShyhqLX$PSBO{h2r<`ZWeobr)Fn2 zP$xVecK!)Uw_c&tJ1+y*`?i0!#iADOvr5)GvDviXBEC?n z`Nn5Id~M1f8|({pyA*3#GJ?diI9neC*%5SoBrL;0_G{7PsP<_?khMXJ-qhWr$2QQE z{~Ap|PMK2_BGL|bi$m4W_7Gw{{d;>GxBc}$Mdw0-Yg?^5jqo%VRE=Bls#4~+sid(t z4SH+TD}%Wt zfxT*zm**Z#W6REncbGO)kL7?l;pa51sQ=+6N290z#6QKD)OIJ98ZpJ`YKL+PR69XS zwRO_<_gR3QW)T@0ni-snUP#|7v+o&clF_h5Yu!X^agv#mx0K+t+(Aipmg7}CKy4?W z=J0n3Qz|T=$HA4mqC@)_++V9HY4=S<1<4!oLe(elNv83|b>y0iyXSrZ&^< z&M`gfP1`Vn1+uT?>8p3veiNlVV1WD^MYD6l$C4ra#)#`a$WiDGz33mP~qqx3P@9hvi6%6?YCGD=!;8-x= zxsLS)GY<1${Eu`$SGXCs{8jI;3jay%v_FzaZSGotPup~%CFp7eb$)2Tal_V5O3O#)gPJ?A+Y*IvuyP|f@)XE^m;;vpW4aUY2UMIZuWS@D5 zYW?nq7)sPlLXg1*tW9Oz+8f;H@zK z-$sX2FDOA%4#}g`C~_pY9WI0yXIP>vh;mY{3kJZebnPBo;(I#5-}pa`zMy!m?1odq zWS8H`$=MgjU$@uv&FCCi-b*ao`^AG~^BGj&V;ZT`efTyP{v;yTQ}(E!`JUU{p93ad zjFS;k6Qp|^q2*Kt3LFN00p}UiKWZw}u+OxTKCg!>5_TTan@*eOE#J3SJ;%Qm@e}gi)O}-GkUZJ)Y;@sSPJb(#JQR*tA{KD$ zgvoGpv04kkZxFe5?Q(bJK7lBfD_p4=z5e#%doOhE^#OepltNBUDfcEG0|l+t_v|f+ zMpW9Hv|V^^KL=-&HUVV?tB10@v^$I_R{*w@-4p(RD2eVK6V=18wA;~^PE;OF`#yJeX!h;&>!5^}g zFrssBBpdn!=4krp84b1qn4%(Bz*m%=2L$6vKv}zyqWUae=KJSt2G)E#)*e-p{g9_r z&_#ULE<$)Z>;=Q$AG1GGqvMxl&t}a>g3<+WwF*s(L-DKf@v1&Nl^?qc{d3fg)KaH+ z1^xt=LiqgYd5CDbV4k^r>RvSy{~G!yWX{C>E3koBPf#yPZ>3f#$}`7gj|MFxr-&?s z>g-X+lnBLQNQ4nG{eJFw*C9@^+yOpN@I zx(-k}*h#$IVP(K)J8N5{GL>4r*viFOPm3|pL4QVBKzfu;epardT51F4X+&y zy`7|5Q-s~S)l3P>6WqYdlTANuTte3fHPqTLnitU^(qtK;tQ3mu@2fP?QI=}w3b8;W zrje`1S$w$Gp_1dM$I9g}H)2)!&=xZzZiKlvqHZa~K}{f`sDhAls--2`-n&SpM!Mrr zgByVDbwlPJnMTsCr8BoH3Z}toE18Q!nd4*#(ejywcLveBI@iLk>+W+-(07(*$8%80 znD}i=g~CuQK=2IGk%0NE|Otyx7ZRNc@;SY+7ieRJ5B5Iy-ncR1| zcI^la%mm@n(%yMs+3)d)kr)Q9f;yd^z;V0?8XpYWtgskXl3;e>Dvd#N!pisM$J*kraUj- zR>IvT{Rg|G(|Y@!b+y|-YKu!5<%~b43pb?~3diW->I+uIskXtWBF)>T1uzIILnM_Y zI0R330n5*XX^Nruc(_IUyWwCH-m>*ex+n4Ku0k{zZ21Um_8VcYHDgFYUI@=JyCqo< zJFyZEhEoXnQW%M7qPr{Q+5k>@&p1b0l>?)=Ss0tVZShCc*cY?(w+}_ z>A^6v%94ur7&TN0HbYdZiqSFDP1UlOUm~#(5a%Ku(A~Xv+3z=-hl8R^WJm3aX6|15 zEMv8)!dTgqXZ1Chg!($kj*sgSGsiC5~;TZFFkzU zdVga$ii@G%-fok*$VEPzT%IMV$r)cp6eiiwlML5quOY*(<`A76AMk#30F)raTPt2b za-0Rqs~;i1P(}TpLM*<&CS%`p!;kw4qx(cGp$VgO@)Vy|qCL z(JCeo!nO2qxuhu}YWTYCnxCF*0@pTW@y(eKCJF9qRezgv=}Tzta=+|H9$MM znk$8vs-W_yc0|XOMN3Lih2$u4EK)P_sD$Q3eMTn>k?<5}l&siT0R6Iu{%)s!Om=kH zR-?mdY6v8HZ%jc!B&sNp=j($-Jua}u>a6#99S8pyT`}^cDH<80|F@Dly_9s72!JPU zMx^oZWdn6pi(9g^w`)q^Qe{}7%fC_pU$o}3Nhh5T1_Ib+qk~@DZAG{f5B?F-bf?#35jFu_3cOLaf|O7JCgecOe{i`B+%ie zC~67Os|~Jk_SIfP6n5_`a$wd;G@^T;U7S((&@oD6$s%OM_f1&4bBfT?w2G^0sHN}j zsGFpK#zpft*GeP}^7$`RW&r8>2_&WWIYG-tknw zfBgSh_B!_7v+T&qI5rtkvd2NPS0URuHj!14y+Ra{>~XBD%1Ty7l#osK_+H+h-}m?X z{mCD0ZrAI&p4anwKA(^KgPfH!pKMJBMGu6-EKOksEUDYK$!NHg4chE*H!>j4@0qyW zDL*-?71a}R> z0k#4}afbh;d9kr0lo789B1bVa&}aryRHPr&0=hE;S7`>=oqn6Ea3c8(rI8Bx2*Qcm z3Ic=^iM!U#-M|CkpPfvES0Ub-FcDa2+9x-afkHko^t2$vDWB{$I@4Zt z?4$Y{)3b3{b7KWR^v!_EHR}P+6y7sdxy9 zO-M~4)EJfP18&S$VF7~u*+nLj=`@V(=tF`Vh4gVaI+g~P>;jZ8$wd<6Ka4|&7&dts zXdl(h0}CcK)>srDy|U@u3;{e!L0D=AH9FRd5_!NQ!e!^vjb6eW@}o^E1|Q z2G42ZV7lN#Kj6egdZT5xozSMK2=X69Ks89+hRnpoa(j0W-nfTQ6U6B}WSFKwuG_6V zLJuKn7AOK9je@NmXZhTUxB&+}<=$0}hc}@b4VK$Z=@q%|HcaYu}3&n$HBM3Cv z8Sc``FE?>AL_cI{0Cs0$KXh8MkyD-`Ft>GS8)$UKgTCHSP(i0B7nigv-go+H13oAm znM*5$e)2Ai{MR6KV0o#L%>^wFz{-eEg37BevLPS}n-C-U)x$`S1pnedouXfY@Xs{= zg`BCH>KOra!jF~{9FO{y&i6fsy>5cayjTuGE(s!+ zh4L|6X<}OWiFrd(cUQpe7+1s9k@^eB56V7$1lpi)4*$V)KZmXF6F_c>A>h&7gkB3c zF(GZ>87POt+D6jhygd4z05%J*fC#*ik<`U3xE}6UJxXLXL^RLV=N_u%X@P`{*a(zd zH$#`gObLI84#l%u%6&5fBKQ>EQmFvhkiUl0abaKh)&u#|9@6n?OiC>%x`YkgtUVl3 zLm>B-&pL!i08iUL3UA;Uhk&s-dPD~jU&>cr$?Db~DeZ-;PE7|Ziz5o}FoZWGJ zbGmI`-I@*#CEAgk{NY><8a>P<&PmXjq^*Wbqv2CiZivS)EF%~?EfC%EaU?Mepm!OF z@HV3AF-dWDA0~r|L4HoDyW$}{DPsRd^8qBpRh{6is}o8!74ilIm-Q7E5M1`|-fhGY zdjZP4Op;d<-p)h^SGiy1ulkKQy_+=zEe8XHz}(B8b|38RpZxPJ8#+!6nPnlWMTcVN zh?6A9NmjS7;Yusza6fOQWJ92>AfYiG(aT~;d-g|d%pRbzt!4sEEFcYtkqgDJlacDD z;;?)4XqAMIy}UA7BxnO^pBim+XA}?N9i~20PLV=e9|=_7)1N%DVn2D8j0pHR9y`Vn z9U@QclfULeiu&@%zoYSugvqyCpCN*8@-MuRqmxnJ!M*W#Qr%HY*3PKPO;OFtvm#># z%aP=o3oSS;U3ftwAQPUi+k^Qn0u{*0EUW$R`tvZEq%}Q>iuqDU8EsV!(iedn>N_ zfEEgG?bacb1@$&yMAi>@L;cAy=XH7*P7%9|{rqjlZbNMvnZ<%2rc^9Lu&fkt(uHAV3%6CJN|d}Hq)E7w3YMKb1EKGSW|$xgq>>>39O?#c zR?}ZRu?9@BW}G5rwipT2x2FYQSWe`^e!F4%ha=EIGJ=YExZu6qgI%{ zI+vH-EtngRyGE_G)1>!gC8f_RA?)UxxF#{5jr^t31zKu>o8kGW9!wNi;Ec?&GQUue zVows|-;85I*qYMd#L5@EzmqkKu`Hq8$3eKa4&UJbMkARQdY}c!c>+UbS(O>1era%2 zT*c=jJY%E*3uyq6*ve<;hKpB(#S`J5NI*Moe|?+lF4jf}m7O_lRk4132RFu%hixxD~FK#>R=ZvuuI?=$%u(0&mI%6s_~EyI?gaqY`GN1+miSkh{KbOSbklYP(BbG0L_)l80h5-d9ZLn=+||eSf)lS z5~i`(Z-!}WE z9E&f2b^ZM&Qeu-jj8(`kXqGsz)^CC-=*2_3weI(mG~vrswWVr`AON4$O0?dBz*&X< zS0{qmRzz2Do7%|dOW_@<3t#t}uh(9XA&7o&VE0L%!QkdaLlB^3I5JcNhbrc2fL$CR zVwvQV!i$uJcegP#u}nfWux%s8Fzz)2)ZPs!EMNfIu8+T9=_@o$3SQF%DiY7S{37!s z9#Hsb>wR-CxJ8hofLDb<1yCZ?ui*=M^rLcUqV^*J@hXWMj8gydXb%txrGt^e#mUF6 z_!vKUL$CK4VxoZj-H3;8615EFpE%S&fpAqVtufd^b|J20mflp_rwk-GpJBv6s?lKNS! zlUf;Z-$*fUUf#JcNhz3FDk{IxDPOzC9<*rA-VFPN+yHMgIZ_`UsE<5$rE)m&=I{*E zgwO4#*K34M0%9;N$!4N3aH0`F#2Hk00Jai`;srO76Fq^VNQbxu#J+`dj7JQb!ZnUf zydo4`DVhnq-V-evArPXtL3f5#8szDstU5~uMlDx??>vIG8}5hQ95Kc{0~mSC6f|{8 z8$MZn#T@M!wpb0sN;#dZ#o147dxrtatpoc0e@5!@wSeI(e7PV2A8q|z!yPhWzrO z_Z5Ueg*2a0fO7*EXGPBndz3-H@Op#rEMFdLbKDKx@;&`2t>aV+t@%PQ%NdjpcPZ2- z&M|?r%Hvfzp|mLxQ3CivPW`BW{WZtXr+E%!y}QqF!wdN2dXo7#l)YF4CFxxzhCW0G zhX6)-3_J!9316=^@ONHowrWmJLJF#M>9*dnc7FTwy?&t8mD9y^cuItSb%5&a?VOR1 zi{dBuszv^d&fkpo;R6;@)U`#KxvhI{dGaP?eJAj^@0eJJh2T?oM-~JD~b=NFA$Bl4AyEWfODg{7xPvEMV%yV_F=1wnlfq z6;y%`ULp9NS$s1fi>Qa-2dTD^6cEL7gX^9sRz?}0750r~*Hjmb%^zZ^KHBDq?Qs@Ss@l7%rsC5#h3CLXY$)nOs=v z-Vj)E@aO+^F5D!Vm0J0>+F%ZK=;jwt7Dr#x!6z;ztP@})dmUi$nM zGDi>Zv*%KIqH6F&RqhRBOoIM7(_`NWRgM;HqWk|MJ`!0c6^~Ruf7AELv-sKpKP-_B4#3r2=Hd_aL5VoJCf}OsGiTg zyYwCs_SLg+fA2+G%!Qy>DnP3pX_#s>SoEj&vZgN-ajMR?YhO^ zSIUIb{bo8>@$art!Yh{ZSrkV9*3K(*f_&!3EnO}M*C((2i%Vz-7xaLI;qEhhJLCcvO>qUGXygk|QJK^A59}dswUgIUS$O2(hz|c^ zIQy-(cF*=XKo;~~7`AS{ml6S|P7b`c-C0XE#n8r@{Ov<;BUDCJeu2%2Zv(&VTU{721?>7!+TYY1B$U7xUcF!w3FX^GE(igHt9B9a0mBNs)z}%wHz!-RW{({Z(Gui% z7?gi_LytW6Q1Q)y4&f>9up$*C3S_`6sulp?PXCfbyt$t$Q))U0q`qWb|tdk8z^E`zi$Q?GFh(Aqt!*{pn%#HnP;8dNFQ+Ie-kE-;x z!E8x^!;EihrIa0K9Vsk(;`)km#1_lVlhE_9dqQq_h9F~OF;;fu#@UFTVM6cS{gI!0 z9U2VPkRXF3D*z{;fxW{?`ZPLy@12(Jt- z#|(&7h2CF!g={HKtkQXZN_dslLMg}4iRV&WRW>aLq#|I7Wahn65W!a=9F3lv7o{_P z_%r-Yw&be0&_y(<8IhRxz%4*Fg8t&9+(y-imj5I(iBSI`kMxktp--k4DU>djWGj^%~)7*NxGzSkML zgkXpk&U0#c3dg^#3d$xjB7baww>50NCtg(WoVC)Y#l zclW_>M0fB}TE>$cA##=%<*w(CQGc#jG63dCGPv9+8};GpAO>Ex{&@{C`6g2I#QP%r z%>l&j;7^t92f|E1&Pk8S@w2WuW1TTAU_E`n3tpE}mn+$qQ_WkV1vcs^ewx@y5-W4K zLc5QNtKlVcWmfyX>iKIy#=6*sn(*Hm;jgdb#v6pybt+E|^U@&qj-UE2D4Y&zr#AbZ zdcOF`oL{~MQaokcILC#mVLa<9L%%tBC;LqL2n{{W zxh55k7*V4p6oYze!cz#nyov3MBjfCdV7d)jF@{5s0x|r*IA@q{Hj;qPZTXXs2WBBx z@Dm-5iwerGZ0~$WKY{2Efa^Wp{^H_!_7Fc&3_6n$p^+;%+#_iy9l7}iE0P^s+-kSRFz4Bh4{wYIJhC->VDW;W{A>C)l08G#nw~7%4tGUP*GFSyQ=J z5b6=<@|h3Dp`;BSLvk`7w((6{{-RsO8p?v(fR|)VK;pYBlKA#eHPGDoVH0EjfzUh& z-#nw}!KvM;8}Cf|cByshX1eIqB1wJ}wH zfva@7wdq8y`j$9v*LO-2v+6~E)JO2j-2|GIf2&W(OmM~8Xt`)LK})>zf1PHvmp{c- z@9#v#yJ0%rK|w0M3S?c?5t_iBRs+yVq6>8FG;YaW{N~50MxVd3o?LXWIoN#6dhR$? z-sq8SykIJ#g?4<$k*r!h&zoVZ`=2S4~CAx4nao5@ckCROt;T-IJI?W z-A`uKMmSZlzRY7tQ7pkPHR!?USD!MXCq!N%3l?Kg)EIH54NDCf!IxY7`4;MGtVp?s z!|r1{#IWZidHLVEBccSM0+`G)tvX;jXYh%V0oEd}l4jSg#&$5HO|L`^X7Y9Dv!$;9 zTSzP^AD?pYD5`suMV}t6>l5CB#~@L9hvr_`BKP%9Z`)kVr0rtzY%WA4=JQZDE1hi^L$*2{ z^1SdKjD$FSMwX?5S@&Inkb>$t>0e4Kj>6^#Z{D-ovy-J)IZo(t}Ot*6!#MSpm|oVo;XD0vlQ57#~B^e=z1Yi21vB;duwm%oeHJT00E z`oviO<0J+Q7u#VB{9(G?a@z$(dLbv)^I@pGngL-G>(}O@=bJH|a`tmcb@te(; zcOJYuA;|Rx26-OWOkJs|lm;k9{J>9dG}R}|%Z1x^%Lk7~fZU?NVa6P}0LH6+NFNQI zojK+P@UeclL{s0|9$9+#>pk3K<(9*=e_-ut54}5Z+H2f?S6`ZauDTcawSA!_Nh!pp zM1wdT$b{igKHj#l?bb7^{d=3hfe#*YIPj|py)!Xk{e(=3-<{sl>ms4P^wHe;@(ld^ zgAg;zEM?rn;#*2-~Y8Z2KlbuH+jRSH*`EYIHc}9^>mu1jb1o6DChE*=TNqY zT@(H9C+j~H{rf_S{erjM;hw@mbFidc)!J3f`WmI+|Cx)=d39mdGIuK~r4)Yurz3vA zzWzS`hTX4fU|d<@?76fePO($8KSTS%&HnZA!ItLfzpbFIDgxc0$MM0j2NZ0Nl-gBJ zdd8Dvnv`lddS)c%YpU`>r-mMex#{_r91i1sa17((*=XLn2z84+{QQJfHV~)S^w{ys zxJj0W76mP>XAyTZLWcIR#D_;E@D<9(-kJX&ubauPY{-VRC-h+Bu-5D>yKBFSwIu3q zbVk8^DyS2bUhIpeJZO(}Y)M^aDGiHj!82WU6gBBZk2PpsL1EP)cpV(>Dz`QHP}V?s z(mVDBuc5^+4$Rm-)Izk7y>!fL>21>oFNMOEK%(>0kr>e#cr4!268j}Wreu7kSGHGW z*nm&Kp<&BgkIwu9$aO`6|0TA2@ue|eb8`L{UzVk8LK90)8e#!Ip9e*mze!(@ZT2Gq z56@rFE_{i4YU-ew*qGQcE|l{}93zcY}18qBiY^@1ISsYdA3c+TGhFb-pnHNI}Z0 z*F()4Q&xqoj(HO;gmAxVy*{tJ=MMO6QGF!wVQ_vfQx>an+Xmyju#fQnnL4!E$0)%+ zdU@Q1dq|am|Bw?G^nrRIbvN;Sdtdzcp11}NptIf}CfNoyNesDu>-H4yA?YR_AJ<&# zucWy1A9EHB1Afdc0~)Ov_E7l}nVYH&9vwml4+-R`hcPd`}VO4wU;RR`Yi#aAJ ztxsLFNb~On9~HW}*CR>cLW?HTYTCNZ?tQ@I{M_k+yHZsJP*30UI3M=dzngXNjbGt5 zfAGf1{Rfo2A0cBE;r1SF#~OR>PC;jJnr23(n7ok9YkQcv6`~1uQ$4#bm|Na<&0t{a z3fuYeLgVFOV{q~8igsV2SX!ytHz7dDvq`_~&$TL566o(Q!{98k+|>s|%IchdV*V%E4lYd#ZD0JuWV z0SKSej3{)@6?k{!LgPRNxb7gE*B5G!V0dk^_}4>gVBfQss9M5W`<~Q+4)6rxyxOtX z^M$Sc=25Q(!*td7D?s1T5|2z;m21JhS~o}Xd)rsgl`#gEVW9Q?=H$tjlcWtOB2G~1 zSw5L_eh#BGzMjj0kUoi3B%QT@^Wtpzg9ghdysrIJ*teq-I`ukiTU3r1&*l%2-8+|VI009Z zy?e=4d70Vibfn|zWo-&Y)!a5MZIy4sXNc|i1VGhit(|3=~D^7_-hT1PaJvsU* zA32=vgYu^ox=>=3-G0VAu3nI|;r9R2MoND)t_D-_;C7k+X0FXW1;$aC6`HJ+0)-X! zPTBRPyM9WOQS-H{5F{|(O4S$uWfrIW*)V^8g>UaH>Zeb**a8En{69|;e4EU^j+5t? z_uj48FA5;q$8smW?Y?4A^n-vNM9$_Oyj@^h8}rO_%7(y}>o=F_ezu^AXmLg-Og9L4 zVvhyp=Z=0(x2aH)1dt`?zgYa+vOtT{|LN9dQ({{&#OG)-*W{vP?&BYu;I-}dtsy4| z@oetJIJF=R1^Lv2{bEn0hQEE%UqVjbz%`Nr(DNNuH0{fxXW=1}#M{+BBUT5m?wqX! zkCWX#X<^T=2G1nXC>vhfya3d=S4lCGn?vw@4ly5{P^K!~?IDgDZsP52O4SwZZ*QwE zb|-hvJzEv-pFd|lF?3TncxNuE_x-Tn@q=4-P9cBL$6+Suvo@pmM_HZ}sgS>pD_4k$ zjpM-FK*v@7-E^iGb%0EPWYxZN0fq#p4HVr^c;t?D_&?dXTwbpmYMzm}m%t#KeK2WeBe%Tq%Ja>ivwx6_)HZc} zZc*zRjO2@FcS2W0zSpHJzxwia>q(fBltkde6Q%al_x8QpMQ5$Gr8*ey&&)oBPR%EN zk52!6ORw(XM1>W@tfaeX%XE+C25YB6pqWXIcl;td9u&5oqaqo=CpKw*zctNNxO}D6n=S9_`3{i3-8~zb1!~Rp)ES=)b>76 zBr-1Ewl*TRXZ1JHd7r!08eOrLdrtdEe^MW&>N{s_=b8EdU}PLlpXu%M0%;xo_N12& z!O-6WfO9!lQparAZi?0f#xlw8v&n@5S0pox_PvTHbld1pLA8)Hii(sWX7YAtf$KIp zLsL~@qD(>{U-^t2Om81`&Hb`6Qxnz;vNSD0Z}G(8yBKGV(x39`fOk+okC~IA-j2b> zkh81xMyBDWNWAJlG;O%OHR$-={8F{|8ynq1NQ~b|#eJ!Y4U>E-symmdvQIx3Gf16< z<{|)x;jY;$+)I%>O;HS92B1zHPlXCU=#eJ^(Ef{gqc7Kz@iKBRr~X{rHM3$tXht9x z6y(oc9VS(b-L9Yuw6?7V7G_o|*6+vwDv!0L**WG}rv~O=~XG+=+1 zjm!TeJ=d%zj20H6f@B-lS58fUrw zUwxY47eZvW5$WXB#0+zmfZ&-K?)cE$`W0$9 zx7tjB63$*iG4asR!P?6&_47^&MuI*jWDg4f!3pCZ%o=R%j-FV-(t*emK1fZb8g z`>ZDRRDRq|`In`H+l8-fMx_BYAmRq2ffG_Er&S)Rlh1fsAE197X!4iL4<{-V+92tx z|Ay?@g9tm5AI_&bJYGHSRXnK9%RbP{-Id$OJD%HL2{fS?MDmG-(~2b5F*}d0ebh0- z^d}#(yVRIf7nTo)mSxllR<*Dw!gWodb74+*9PU#J8PzeLKfmdk?yWLAWqLDU;0W9m zN1jTTuL34@DcNO9@Jt8YB?uM`o&Xh`jPbmwM8H7gt*s_2#Q*`^{hPNF{(Tnf4(W-1 zp|jH5sR@W+x~KD}Xg(4#=SFMMcajl*BHwLNz3sk>8KMgOd|P7UOaDN;^xG5g2+%Tn zy=gpQyxrp3u{R@$=fga( z#h$7X#oc|P3i!15LQc}RL8S)$xn2<&M=K?jSCZZicJi2VR}JRbu}qQ#=64UIJcIE6 z*~wOz-(Ql6Z?}5@oY$OL^kJ9EoU6?mkl^Z60BSh>iy_ep^BV8Hb@iw7wFF3DF$K%g z8cvlQo~{d#84S)o0wV&uzz&%{^JBghs1JBtL*_8bHR8KqyD9^6<}%AH-E2gwKu698}k zeX^@IbZ9QBg&}9Beh%C;_5aiIzhxSHoknt`Jo2J-{yH z1(n=@+~!?Bw2FX(e;00!~qT zT{VDE><&|y5@IrYJWZ|C#D;4mwDoHSUg^j4>(GM%UlxP4cZWAv(d~_4`{`OAZ5E4X zL^*xwWrRz^9$Q=rtxG#XU%p~olC-04&`g7Ow&0?}p2H#}3Fn1Ip|+r?ovqP)CASu* zJ^BbQ{9ruaVog%*y|DIa-0S7f^XdflLrlmUo3D&Ev(@o<4GoW!)xV7X%Tk&(7t_4G zOKtB_HQ!9ce0w-U(mH}!X;V2g`b`@9KlVpo=*{cy6hb_&ne|CS>t^GcjDJu$!J1xo z`#xOxLuo)xnng?>zNjCtS7m9*lv)3N-r9d8Kw>`3zN-$;GJ2iLNf2l5^Zupt*);t) z00A!R*ji056_z*Y?bjB6teM`;-j1TrX@9g~l0}m4g(Xi%@e_F|_YZs+x`-hWZ9 z{cqYq>h^7@qJOd3In7-&(vs+ba=RP9HtB6@0|uvS24MR2kBEH2A$z%`u$I=Q=eaSW zELPTLp9_M6d|!n=62>Kv?~G8buuY=_N_8%Q0PO&8a&xMeii)_JYT{qT+xHFk$AXw6 zAzSu)KY54Gyyu0kstuY6ER~`0!%N!*`;HG4zU$=AhP=@K%RB6kuaDohe?!G%jB-xL zUs%dUS6Z2BJl#t{oJ9T~FV{#S-k=uD*h3=xh2BnU_59cNBUuZm@j=%3e%1SVOT>}S ztBXWXDXddOM$<}(0T%>~d}}jQFG7EjG~Ztw!@~z#T#bm&(s!gqbIEzDzu0>VNuLlR z(wA)G{k%TZJCzPxv?imM^@5KpM$3D;K_9?f_{3$AxoAVYdpa=>tx7bV_R00}_rmsU z2`F2tFauLP#~RVF!oGTS<=@wvpT*BQl*Tvbv=m%i`&DXerm250G6a?+t&9$j-!z~k z4YubwF>A4Uv>J6QtL~Bal;s0Os$kGL^V^)#z$^1V9r$C-G^knrVsuc^jSbgXUkCxw7Q(MExI#QQUK2M~p9vbI?iHoD)qJf!s?`3wsVsj`vU6gKuf*7|oM zOd{I8XtSy%j2kZiJEJ*M#}uPoz42&;%!1w~G|F5r(-4SPeTv%%Up5K&40cSXPz*dp zJJ2myw^lj3%zlknTy`I<8h)a)HgOrqbR!MnU)^@TRde5_OG|hAbwPig8D{nJ-h6P5 z#?Ef}m)3s{m3BaF9ZTD}IGey7_uz+3^ z3BMVBu#!Ypp}^AxEL82;oO+(3Upd>h5q<|nsp%xB=TzOnK!pfD>#TY8CU_m?LN3qI z|DN5w+YPudP4B!E9Mz^(djak}L_1nn$>B41(RweYp4P2ltz2-Wl!nIR0kmYkNB+bn z=IQPF?JtQa3`IAg*oNV%tQF?;V1so-$=I@KqrBqV4Kx+{Wc1S${XGEUdmU3aX!%Cr z@t-%Z!gj6e%yyjoDc{mSOB8OaqYWhia=E!l>_wXs-IVA&MO;j#tc=|um6y#G~in0qaJir)XpAqraY>;{wX-%U9n@dW)ComBU|5ju6kaR zLuA{cyHJ0PaErINV_EN46Q|)JCE&}=id}8=;90!l#S-8V&E0l*qcr233i#{BfeC~f zYIJFvE^|p3c-&O3+Bmn%l2q#sG89w3!m;X25mqOjm%+*3P+-k?pyi_?umleWDCM3v zekr8!DFKf-CGrlt(GMSeC=l{n0JrjLH;;(GzFQO9m13^ap>8&7Ac&SqrxvA} zNAX>hY^4>4Fvyjne4JM{C=F;x52A6kQ>!+1pyp6)!7}m*))Hi+Jt_|B(V^;J%*`%< z01)b1#x6+`wyPhOss*a_k! z=XhrEo#UFwQo#m}hiuK7&>j|zE2Pyzq`|zg;jx??YU^!@GCjV1h_BOQrDCGAarGw~ z8n^m4ul~qfnh$d^=rLC4IGuEa!WFOiURs+g zZzQQ)?Y?!~(`?U{)hD%pE?5;Sl@yn#!~;CH(rM7)?sQT&?lS_nzW}O9mg{xBZ0xGC zHxKxuhuKWX74W}>w7I| z^rV8mn>o%t8q`~E+}8*?y4Nx1$;o#$E;l>Wl#j~^wF!X zo1@b6kEl0NOv>t3;_ReNemEOc5nH=BsxVaXZqLxjp+e$Nfv^1T*>DB@@13NdSr ztyLRXYW33rV!~0j+H8wTL|+MSQ+tJ0zR10S}~?WinBX<5j*?E{eO`*h!|Yy zlC(#^;f0Uu{~PIOEpislcJ(*OxEk525EjTX8EeQy02vRX4IV&L-XYQX({G;}xL%q6 z?m-6Pr3r5OeNfIU`%fnETF<;P2^9Z`+cmQz_OP}nd&b7!MR&;S4|G7UG;#$HqHcl> z=SM5)>}d9n>AFB&cQSnb@s|6E`kO@SNp=sm?cMp@U!t0cjJi?Ihi!MKAHTVf5%Khy z1PD0(N)q>hJemblg5~?e!MX-s74ZWmKIb{>3k5c5Qe_TO4jTWh%o2^kxps><{01`Sk5X#t+w^@;O< z;T*T5U!GU36yGemvDn`vUU*3^+l&jN17y-?8edvQ052fDv{!QT8vp9X_jeY0A+~QJ zd%vpyy6wv_iRumZd{Ja;(AxgQag^1!bN*t$Q=N6iYxs?tr8T+8X5+G%9WhV|x?WyK z-n5l;K;QWLyOP^9?*dHm~@-F`XYq>+X zJ>qFTpRv8>ptRA^J4O+5(8&MqevM@b0zir=5<@YyY3)x#2l(?s(PJ(E@jZXBIdL`& zDBXwakHeg&>YwH++wcWy3gBw_mmJ;;0c^`+|4`WGap|#C=Ly&7Eq9lCY^Wv>_H#xY z`A>T=`|H^5EDIcleX##?0Px=Sw8iu1b=Z13I#1jCefpRUwvPgtcnt;bU*@KT{lH=C zciqIGe_zv@$G4p~XA0EG1@83BZhU+j>vUyk3G(*bqPjZAH+RXA+G@nT9349*_H>&d6k5TlrvCqgttl_?# z-83{74wkbWfxn<1g>Y37h9L<#2s+f4n+AqbFjW5wg!`m4@DR}%aMEGpl zlE%mOred8?7qv*+yO+NiqVL8rhS(ZCLvkN_$1yG0F3_V4#GDAosiFzgsME|PfwPR% z{*f&h>cn59Stw*st0nAv{NBG2$z25PbdaaH6M@Tws9TeEfI=Hn-*-mrx=5!&WGKaU zWQ#a#y%LtAz@dXLRv{uMyM5rikodKm=)Mit8pp7=UvYB9(K-BUsTqKV@+auUB0~kltRvC+W&fPWq@Qt{-&~5I@!A-my zSdh@yW=Il*K0cv7O9b>hIx@q>%p0|j@)Ce(vl7orAJS^HC?~alOejExskl}*+%Ou! zs~!GZ2qzk4827mgn*&TF8fXoE(E33L@uTa2>bRRmN+Xh3=E*+*T(OobzquB2LZa~_7!}c`Be-Gs!oqL3YA)wE82q!o637YY>=TN7 z^0`teq859izuq_xnhYNigsEh9tKUipW*}4$K;fs{rBXLY(NQ;zLXw~$#*@jx#MT!; zfJ>yN0S(Qfti4VmkdgFQ8qdi}K0hW7^XMdh{$X(rn14#oBF+xStdJT@S3a-E|9kM3 zSg$i;vh2+l?Y7^>2j9{B;;XLMJi{3P=jA zp;kRz`ywm8io=F@g+Z$I6DVJZrJ47~PsZ$TZ?hSUz|LG4?)QUZeN8V?pi&4|d*qC$7FcLLey5k1P%E0P_b9FviH9T!Q4;-XbXkD|(mz2B2A znj;X+rY#AvJ0lQfi4?b+GLAqc;O>YRZHYb2hq7@{$QI#E&Pr!Mhx+M|*9izs>4B@( zJ+1}4&*W3)`RX6ZoaVyIZlR<{P$mO}KKIwZLY0+cHOHyT#QIVFqp<^i@xTZ!U?aV+ zGn9yBcm{N@x=G{Wc#$T+GeGPofr=sGK`HRpLTRNnIK`w;zzYZ>`!NFH8U;pt;~d>E zB*seK80CL}>D#ELREXV#G5<4!vfO0^wG-HWF&Jku_VIx7?K8SLO$p#N)Oe_SjqYr4 zUUx|!px0fSnQUsT@oS`6*zgrZ)9$}4lvOV3sC#Yr<*PL;u%l2~-2!3h8Xdiv1`LJA zfU7A{e${Qu!{}UgC>~7Aq31hOlrYLPgU9q6>@1vtcHpZlk?v#%A44RM2IeuC*4*_% zOHg%fv;3s^)hq1M7lY)$45OVcwb6i*1+z=TFT5X1;)tB);+X4M;~tDP_s zQ*$Bf-b_HfeNtcpRa}75rNC7%0ZD(P@)f3F@xLLwe-{zJyAaXhFa`b6RabZb$*kpA zgAL!}%BOZf$X)Y$ke5%$6CTA&D`3oII3|~PPW1{**+A9d5zp?3xe=i=B(|Ma@HH7E zy|wO7+DbD#fTWNP zz(&#YbeYWsHsenP>2cRr0LAmBlk`p+?!~Q_Gh5TO8&C7q%`GFM>o%!^^CEE*Qan=#vLq0wc)x{sELl#ZVPTu8rR~#Dl*a!H)1>LHrP6FF1u*ywsvRfsgl^N-Csa9QF40E{6T|RXP zwBig)q`)ZVQFk|Bg%bQgbG%$Jv`AR;^P&LyM0(m9ATB~s5nk^HgYtIm-9LR!c{y^m zEz7#9V+@Dkb`MZAibXQlC0vj;(AYK+20;!fZe$~wR5n3@6IxJ>C3dNg>l9mks z4WSQGc!XB+9MN$Yx0C!I4^>b9!x7WUP(nunezZMq%tlmt&LOj(1gFKmN&7TSFYo$Y z=d6uR<6sa3|B|k=66#tpyV;UE8&B>}RFN~i?gq-*&G0gytX0;Pj@abKY+m=yVL1sy zJSW1@&|7%Z)REc-q@tZkwg>HuMD}tJ`rhcWfALztkdxsP?Uf0idaBd@uI5cL(x58| z%sQw`7ML$3t@QFT;31H#Ed0w|0H`29*8LVq|BVBu4kR!=I*AMdWJm?-RV@%nq8t(7 zZJqrPEf~yiL_>ZLdTjtYEJl%BFS3iT{}wX$>@5}_DVZb%)0^OT*&e3k${&1yXyWmk z!Q?OThxhQ%3K%dM_D=VP5(zbO8q~IySH=-5k|OXWOmQ5ASnMZ$hVo!EMgp|7p%HJG zU2cjYb-a+CV2leX%)=LtQX4_oenP`25Cw#>y5B?ZAsE76b@#|71Ter91SIjr?^5;X zr>MK5$if971tzu;-dUw#L|W@G=3VhK=HhDzsuWn0wj-FqL#^0Bcy$qyV+26mDAx+1 zK9S%2(QHi&dU68`-9Ubq(#1wN@eWzza)rsYkz_d(aoBes1|Az&hZ*s zP0LZk)ZIWx82sLUiqify6z0U0^Z5$;LhOp?L_!FVK088$EZ3w_pOIosBBR(3f8dUa znrG6i)2cJrBCnEXNha0l=)^q34g^utcSTowWA7Tf%8TO-?; zX*A)>h3y<52NB`)dUzG*snt0OGk(BqHEavX)$$1mb-+w22=QWR4Kgv}6sq|Ih(3&f zpN6tg$rwii3>j#}V~j#;aJVtM2~jLIUCDw+X<%_$u%1X%fT3377yRefNSxA>3~XRV zB5VeoYAA`2VYd&zioxk9<>rvf0zQ>*;ptZmB!#v+YM_C1oC&DH1@(3L`+wbgY=ARp zrRDbT?48{!p($N$E(0_a47?s6c~>P6GC9P$P7SZ%ZE)cbKIUtF*Ynkg;UOa*uBRKPNdZK76{T%Dhcbrh%%}Lzefv8snlDzWPdI{NHfQ zJsJqVtmc9+ITLCI%eTfoEhQO5=9B1#A&652f-<$3{EKk>rDMH z{{R}T#pkU|4rlwML=pc~8S`8Xd#~7lSudiG81>`+q}E#;P6(T^QT<*%_AiWiOZx{l5=KEG&!^!*qV81I`PuxIJK41mK2MItUrz3}b*O`yD_~XU?z)ul@ zb=nA7KkJTN#mHy&U6Z2K#i(ZQdn!n773zWRQ^*YdAu5rBNg} zMT1%y8uAfnP^(E6aQ___h6liqRht8;pNcmb$wH_^iANrb3&Q=!<>8faox$$`y; zS}#VxOM}vAd6c6DhJ0ehV2m=-;CRUF#z7JOzq3LK3(stu*cw$9_G-O`b?yP}e-E}z$nrhU!3ay@YKwrD zq4OrtFhGM*oFAcyFetOLsJu7q-2gkXMGZ%L2fjeB3(+@e!_CmQG;RPFa`utvf&fjF zW*`2F$0Y9(VUr)1smNU{6%FUw{q%GEu2-C35^8;$iL{xDrDpHF4L=Ua1EE&pc*z57 z-MMhcWXYI2@#mrJAh@p*VikFQUL*3}EeRt6;?QML41+uL3W0}*;-lU-9xmi$)pNaqDA((3qs+K;W0MTMg z!QGZEVObRwrU?>YYCibZskUnI3u5%4w7m=t2;3On#p*`L{~3QCzeFij;+SeWyIdk{SJ59(S>m zK9zC0zK&xeZNNxd&nU|S`JXip5`^>QRa*fbzfzNmgFyvvJwDvTc(LxBIR=ok9w$UyOo!s48VV0>?@cL7{_rMB| zh|rW0*#lEcjYy`4dW+G$;B#o`{^5*C&W!b2(3b)6gJ0mSPQHCNRc{1D(3f5nOz(89ccZ#kJW zw~!s>j&GU^+Z=#)x*3KfzE#JT_QlYUGi_~4kXK0%hqk3)3^@nz7AG3p$yX)Lr_fQW ze=;iAvHNo={#k`uos{jDT}&W7!#9W^d-Lk*Ue~_? zKD%c(?+>NssiIlhnP* z@vn8aTu?@!*Q0*UZ?CSCOu1tuwuz=(l$sxBfKOXvURMg#2*1c4a$uk;@Cd@><4zNx zEFz7SvAo`$Osx725wqGCI)*n%0^W~$Zdyn*wJb-xQ~0o*SO-4r4nmFFwOt%`_J4SK z?|7>J|NsA-WA8%99@*n48QBuDl|mG!?7jCm$B36CN!c?qyU6AoiOSx4MK)Q-JkIfZ z^!|J=zyEUn;KX@6p7+P&e!pF>K;3nAwCCrT5C?<$eWqE?gz@H(eeXmG*=T+?wj1Iu zl3(pbrdV#444rtkWl3FfoO>N)z?)fS6Gy;}yjhT1F14`SuC2lRxqV(6^}2;(Zg?!p z{9G=hFS736@#H5IttsN+v@}`&3E=hr7tL>1$V1idd|)P>Nm0TbPKY=qYtC=!!;l9X zWyg{cjHdOZ4thnOLglvjm0h+=G9vxbBAnL;$wHb0!udYxOgnaG$u1;P|K{=M^Of}KA;JIdv9e`c$dNqPB?=e&uIqc{ zl((fi^>vE$&1lxlCA)%9j#N}ZW%%tJegrM0Fv~jUO;oE7F*ky8ul5a0@vwjhUS*9k z=775EXhHNvRcrMeYOfBq)DSn(`_;=uxMf6tBC_hs3O%M9Sbd-QB`a_>ZMWq_fQkJ< z;=An>a$*+B+`Hqyl3c-DF0XRCbW z`HvJjW<9v5psn0fYWm>I+?aX5v)wXyBla7Ie#SHE=zwi#-t=?zfGhnZIKy0sv-*Cb zT?UPVA=6tiE&$s0sAo}yj6kZepE=X%ic=vB{Ew+BAk4`Fbn_G>E#5VDtqxYl0HbFz zDq+1WE|y`xlQLO-CKb%W^@)wRR#1vU(ea`qWKws4b~v6c-Hx)dLS6PsW`7ce!h$F%LazUq~DmguXtSo4%DyRlc4$j zMebf*)@Hb7!X-&K!C31le^##ZN(thZ5c$j}%=lPk?IXQ}mIHpzim5nzGcfj`a5 zE&xLl3Z|R9d^JZuGUE6{pp}$5Iid-#7(j-OY#RQYV><(f=U0?HGO zmBV=KEB?WI&o^e7bFSMY+yJDJQP*$o51YO!9Zfz9p-!uILk`Ltd-QPxMHn`N@LSfC zw`wQ~F@EJD>|{_uwFYHe3Ne)x%IbE0P&$A$h>b@vAYhcOFjAtI9Fh^?H!M*U3Sz&8 zL}`~XUV9^$LGs#WBAoT5=dW0tq$H^3qQE0h0qlK1F{w1D_X1MCA*VV5)B7TOWNxiugGK>R8lXP`UN>uo4gKYp?^5qHvZIG>@_=J?ray25@y z_k?5Cc5A-nrPb5mIe4G7eWqvU2lVNBlK>BacB;NcMR!#70N8e-u&xIB@z}u4hR`~bO+C2 zkL4P)c+F?`{raum5c;D;Ew0**x9k-K5V#afOjBOzSU>ebL`C~wD0tf93{%2$Y(Y5a zom8qA%flzTE(Gj``p-1l${(y+gw;Pkq*u7Di<_%;o%$C~N+{Sl5Dlvf!o%=ynIS^- zwe011Y@GkP?Ox(q*5yD_ajW^lqa~{9*z1|{ZK*iIzpNBj;SRIu#a45TKu^PPNN%a= zuGNsb&T;8D-f@rvdkMnK?k*B}nKdUEzxj?CxJ0pgPQl%w=X{teg+RtwE#Dn~J79ot zuoYZ(VoEW8{P<*6;Vl9AztlA2G-JKnVT%=E(+khxg!`u;4(VgQb@V{&!l^y@LP*K* z!OE)yJHzJMHI0@v;IOl(-d3+$7+7sN+8r{+FZOc1&N;YyMPF(2#O-SrAZDFU3wU!uWi|pH8z!im~XkEAedpgt95q+miQjzvX*1MpjGX|IjZq5-n(7S!Jh=~*;_krTwI=x z=wmPA;H`@r%$gg!a`~n?Wl!=8p5xC5Gp9p|beF#Zt4O|!b5^78sfvV_N3ClATT<{I zC|Xa%BLY1B9M|T-#i!_&$xqp?e|WwGDE#R~(TDkWuQZM?ziV+_KITlTrEKy$F%N#- zMj?4gLptLOGXM`BxeRRt?nH1q6E;y#lnD`(lT4=P!qwLUosh>%IFtu-ua5RLe};Az zv4GJ(SCPOH`{WxvR>u}qtf?jPV#I8Y=B9Ni$BSz%+ud9lp$~~uuK-kVN~Rou)STk} zV($H=%~Xrz`L3lpKEPQ`6ZfObFvzIu2LXqd|{(x)~1t`ROL4U;@kpr7aNrPn@JW zY+?TS*XBA12OW{W=t>7sp%;GsVYf*G)gNMJ5-Z zuW~fp@>4JavA>&Mqlm*)G)>ELSJ6170=5QkKJ;M*py|2oS1&=RU`xjOHdp!IF|9P$ zhB-eRm!EWsmkFr$&{>bSl<}2?Q8mql+hCp}@#NskgBHx5Cf=B1p+RaVTdSQ3G@9e34FY}A7xf4@0dEfiX@mCQy@d_?~l+Mpy z)LD=c?0)=>moZJkTTbZqiF9Z_boyXM;o+SQ0a9WLu9cnD8Ml(DuLiF=-W1WOpf4^{ zV>&fehBliR7qfz8X|8+9?#9Gtw={XukV|GA>IPjz(Q$7)L+|t;<#3ljaA-?xollwX zQeM@ldNH^&E6#>6{J)zpiTAtl8kCYeDx??vGXrMJ71NEggdDSG0$(soBKQpEhO{Z! zfr$4Cfc(pV6fIU{9WZi9cT(VeI{uyG1lDdSE+#2HR4}3M+^7{Bs!(2Hf;H=F@#^>d!8So zvwwuzqGOhJm}NvU!pA)pK+frDc9(@A~B$O_2XXMHfYsV4tbwa7Qm^y$aRJVZjGHRRNNYsbvIB+l@-Py+pPLJ1YLtHCm0Zo`kBY<+DTcfp{n|a z^7TP0M?V5plw#-k!1Rl49N8%R>Yz54V{YK^CvYJ@!kBC531Y*dm>Hcm&>!Gl;EZEj zG*tFvZU|b9RWVjKIsNbvhy>l09{un`Led=!R3|Wh8DjqW4_+U^ zxEZi3wGHTO1Aq963MI)6UJ+^Clz}z(f_N7{=XUwZ``&GYQG>2wO<4KKZUUZq@1297 zt6y6R^PR(_%zyR5xY*K{Mw_Xp%I{>(W5KkDQLB=V%K)}spgYHb_;e<@Qx|=!xi`dJ zX-Cn=r#_QmJb8ck#L?*8))`zcB~?fsJq;OUAja9QY6Desc(>uydJ z1zD1U7eMhem}__wG5^u9Poh;haIQ!rlR+8(q9_Cps1Z`D;^&jwyrHejUC+b|-)pAR zJ!-x6_qxJzYrxqWBPg0i`Zap&z3Zk#k4FrjHalih++}>r$dW@mv&;4(>NxMFx}1+T zl+d20V%D(Dz&6unz6aec?(091*%9R+pm4_D-q&0H))NA`75i13`(S^>7u$&wfNP z=@S(0wJMk=CF#H4TuyD(8yuHV5FfU5@HOYH8~CUPg$|D#2%19)A%8RMQc`#MCq8(8 zoAy$IuKuzV)z2Bo>c_#`uo0zQV=j-Sn3TuJ?LkFHIKUlt z-dQx&A?bg_j+5D$Y}KUn@a@nMZKcO)(zDg~C&R-)T=9uN@T}BKe1D;Qv1c1-0|;lG zJ$BPXGOjp9f2e?$Ct@H#UEYVDrBwU=d=jR(T|99x{3WDV!2>5ACGf?9JnI7y*9mb0 z4Xw*F+1R8V%+l~+;(oMQkN%Isha|)%F?#jafA)pPDsq4m)lUk^E0LquNPg-mK73`s zl>2fYW#K2XSSUb*KGlY@s#xwmM zB0A*y#Xxu9$_G#5KRYdz!_#<$uMn#r!50a*ze)imX{O|eZO zSruXjKG2$I9N0D6Jl${3xPO6^g?x~}IBBNbO{aCy$91>h!!XV6d(SQg`IRK58|~Rh z?h0eZUkPivNtoM6SNShkJ^1#gx3TPNYL#0jF4Cb}HP_eE^lB$U0bf>0+Zt?K%N0?p z#(t4BJ;!FlT0hbJQJfyJ)?|WO!B%B&@L%=tL-gfexpT|5re_Zl9g` z4yHHo#OVC1@8Y{s4U-@!&T@QrP7)l%jyngk@vK{JbANpSc@s=yn3RVtaBAKw%cT&+ zfNX#}9RT~n)AOCbr~TAGNy&NqqZz3rbbKHxVZ3T&1TlX7-&2x?DfoY$0H$NGc|#JJ z867QzgJ$T_ZbyHX`|^z3_0q09W>^k@hg{eJ+=GMW>^Nck>JuuDH?+OT*y5*Ni}$T} z+a^5O^Dd2ThJF>jp{KUZ`aiXC(q~O?9eIzlxJltJfyJ{Yn4P*!#=9 z7<(1MceBYH8c`Dop2rT;BUdK#|2h5avwTg4^8vsMrgFHy`v$u;aROq{obq1=2n~<+ zPUNRBGg&ByJA;)Wxx%Z&K2Q>$WVz|*OB3%TiClVKCX`5Ojh@>tR}QgPsaFPut+j4E z?N&v7THjHbwK-C-9ch{gIGyAat(98H(v&ta`Iqf$j6<~M69+7`Yff7CD112A82VGq z$=Y4GvRT+ae;))^>NT6nzD*BhO4@A$6GVXsF0D~?@XClu~j>R-C z@qHS8zpx;cAMSH929xSDFgLTTNrvUMkTdNLy_df2UNexNBH!G5$6+t4MMVw8(=FmM z3wCLR6PLP)#Po|*-3OA<5DbrjZQ{aD>0JO@R&=R_}K8Ve&3SsZFqWa=Y15GpI7`A1S@4Y z4aQ&sVrfr1ABHDg`_ZOvI~E5TdDOC?kymSVw=HG)hZ4f-;=3&8i@6HNr~0lRYloW| z$7>8PhB&pO%5qiC{X8g3m! zrv9MQNj_}K6S&h%%p$UvrVulI7|*G2cbA$t^(Rm#wGgGaFWmMyZ53hvX_h-W zai_YEfd|Z%imJz6LlBUaAc*w#PXY7K&hYvPM&Qbxl=vOTXHRBuWpVDuWmt7nozLli zJm6}fFseIe5F8ymDdhO3+{iTU?)K(z1xjk_I3uIYz+Xj=7fm(Q!%X6lB z*hArPOu5cYftcta_FKDhDTOf-;*C0|f8<2VKF?S5#vA0Vo)V{)`r|9R@iBf`Y!u=Y zlJ6JPf6Ru!$t4}fy2q$opRdVVnSV|jyUmCotXCC`S?#qrY@~$P+{VY{zat?rd*ewd z4U2%xgx}Ie9T|s`9P63^3N2)$UDve#cNVL38*_4n3|ayBH0qQpW?qW_psc<*xzR{2 zxuHMyM&GL_Mdh3=50-#ush(RWftw!-6Bm;+R4x%u_F<XkQ42>G@Krwwp``73BpKQSx4 z?c#UX%8d!qKGZ8~>pge%KBYIo{^7=43|VPcpceyWBwjIrtFEF>qNWsbAsa{o*jh_r zPv;tj0UT_uffC4qcUS(!UYIXWJL*4gnaGxZlzA{x-8~K8Co~Vt@WsN{UMH9*TwMgP z*Sdte-qd2UFP{?2MH2bz!(vp=Oj?wG2R>U;(M=_*>xle)6bm2R7sF@Rv~NRUeM>N4i`#nx54Gc}n3ih-k{Af)5+lXJf#p`6B=zQ^8{AFyJlJi^_bI0QgX|sng2}`P?Z%f9?jG5ritpq2FRm|sxeXmx zk8NEZQqPY8LdVb(KK2h{w_OS;tJ$DuQ`m2Iq8PV!?+fZ`l~WxYKfN8QZ}D#*WWrX8 zX!b8``_@3gC!`Y2<%H+q^f}+u0SbQ|!MHe`dVSINg(0mJCJ*#oeqpQ02pycCcEj#P`+gy2`7Y%??td55 zcJE2HXC48@BBe)QAN|I^qVC%dR7yoi%u{~=QFUUV>C5<2Se+~pY{7X4Fx&ITt%(Cb zXZtKquxpmK?*S3me1Oh59>P$(bwiQ|0Hiu@>^XyhNRMOPQ&{E_2Aq={?|1-A<=y!7 z8yGC?0?o@{_x&jPd8?4?O;F{E(M?N$=Y7RU2HV;Qb7N4@lzLlj)vt zLBl|CDgdG7ZG=KX%WS0Y7s1;0_G>Z-QH*$4LSTCwxPSw{a z>J}vIL%0m3NB0d62^f3i6-0pgVO$>$5M|px;_}=t}k+)OJN?2J@yUQ zzvbU5%7g&BPirBbo?wsK0>cHb`TWKw+^`ng4npBPzrIajWEAK}efK00Y-k zdzEYD3A1immI~}MQ2D=77&n^}Wun=KYB#x$q%ePjOvNwXCkUHOl!cSdcdP)WjR(t^5OTcAu8C7MA`Y=$JAe2Nw+9)M`UTwpdG^JY+!Us8h5 zWMR{yg|1}SR_%~Iibs)6&U)?OI^x4`b!a7X4s^UMy`Ac9!2o1;diU9`$T_uSNcltw zJn7*2q7h0vce}i5ZoOV;w(X~}73pnpPLI}msyv~piibu2CU2QHQC9G4a<-n})AK(H z_w%V)(yt@NR$Xl=N5QghtEML|+i?Rxf&dy)=1_s`eN#0s&>qM7mL3soT=);vQ>Ouy zS+MzX74fk{Fr3YT60pytuO*`6k?HBDAW>QGg=cwO0?olPd!n%(yq>PiTVU8g3ujKPVXm$@B9#QNjnhfX%6!kJ?YR ztE#2bWmp|Y`MKUaLK2Re(JiZ#HKuc+g#Q7yUbEK@?JM{*f1_Uq84x6^%_iIx1UoXm zW-v|jl)7JTQRt8rP!d4qUdm(DxKdQVu%URmzJ*WJGVjb!+3!)?qM+iI!g+v(qEKgWiRU%9H%jt8e zB=c$3bFD%g=MPAovm`ZDjpMBeYgT*98}9nGFg*{Gq{C~q4>V_I9%?yOU80neidzhV zoBYupy6xsV0f$T>VHGD)Yx+4iweRl|N~d20Z~c#BVmmYKLE+==+?wT;7k^w^D+@h+AmZuMz! zmh)S{$C|frH(bGRz%ji@4$JD(E$1pJ$JcdK5^`3d`LriL+LSO00ec=&I1F$*t{0!w z{$0@jXxkPO#^?J-_TOv#IxOVfmU09^y;U3IiN>3HKL&vCXeHr9~h*_vr6MC z6j!v~i3v>ja`!%*M@*J!qUoSB$x>6ujWW!=Td$4*9TBN(+OT!U*gBur*!*+fwEKg7 zfvNopBHf;yuzihRrs05?1wSn(4q&FJKCqC8t{oc4B8=Y%o>++Zc=YJnr`m(g%2fU( z9n(iDvdrZ?M@l~Bj^`E(4M6}(wZvQn(|?>bno^3747ld7t7Cc>HOKWZQ13TUfNmOL zWoUD6e{M$z6V*cLlfcRBKG_llCc^D{bgL9Kh*~)t`XC%nNzCVt>W_?eem*$KXnoOU zYPqrb@h|g@XC&z)sR4in)Dja`rEP5WIozfrxf84{9ZE@%R6T{Cf*@l$wcJzuc@*<$ zwcNi1jSU?0er5YM+;z%|q)CaG1?Y;8wY#=Hz(AycL2GM44|`j zacop+T>k-n2VvnvAwn3&Q>)Nr?Ro9%M@QNNlg)(6SxMZc72)Y$omLp$;~(c*lbD75O{pKTJ3kp^S_EikFhj@DJYl{8o|Jcdhs|*3R-v8_YQHANUBI13 z66~TK2cP7lFeAYOciMiKZ>(6YRoCrVv7cSx3L*4nc$)H)F`JCptPJ&CM zxaoDK#m|NT{!PV%7Q{x34$9&Kq|-|L5}~tN_%N`Oc!-LfS$=;X&Ev~e1M|TG3joPm1TX)RCSt3lKO~&mIeRQgL6{M z=I3X_FFq?N!0?(tI>baSIb>v}d22OP;nGMHr!v%^S-->SW2WI&RfB;2Nl3?sqtb zIdy^$x;VWut`}-%?@9+Bbq&0IfXXQR7u)STzUz0l@xl6N9A`YF%>Fnb5W9af^Ib}% z83G{Fns@thC$o4lJtkteW6r^s1GypYB&V3)cIWTPG0X`6lm|?UJ7a~P(J!-jl|yQ! zpFYz5*wE}Km_|({sm-eRYdImo_G)RVM2&>KtkO_ZQu(J+fz=0&#fl5goP%t5FSsTul+Jev#rSc_q}8`iQ0|Q@0%4r<+i{L@kSepr?~1GLyP=ZC%5+HP zEqRxa8&|WhO#iv=EEPUgv>Xio9Vfv}Bjc{PVH>qjw0M-i{nk8#->%5NyLNwdM|*C) z)#9rkMV!QQF!3V8wCHYxs;^nMYx!EG-0+U%^sUvSByivTsA)&fecKLx$}Wicn~Z+l zdLZTMwGJ)E>p*E`Sq+`H;=u9X;f3G(5M-f;b^F6Wzev2aRT7M)c5k6|HT1THBXMo!uamxru#u>H32wU^f zUw@hzcW#XHYY5ltYY4ukDXQukz?))0#bR*~~FzQmvmevaTnm1iiV2_+|;@*Yb2{^Z5TDL%PI3Bl=#- z9gLbY?CBQ41SO)gf+utVqve@XJae`C+H7Fb&KA;Uw9kU7t-R!klZBq=3QQE^JdSip zxPiDL(HlL_Dm0dh6%B)3Dt>RO zzrr2ks*E2`kC|W0>RLO}ogMZURRA{2O3BTW8CKc*rX7GAlg!+@J#TvJ`KxnpfvUHT z)22ELUcWWg{^89`2d2E|1(0pNi@Ob_2l``UIrJw?UMpF0NBjgok-lT6C@jAr6;jax zztT&1a0vLT#$=YY+asYdWhLt)ld6&9pcy>%033TZcXN+}Vq1vQce8&mKeB}vWlrk`bRSa&Gzi)pN6d+XfGhOOLofD)51IL&`aX2phdUd&k6YDNz3{~C= zy@Sb%^MlBQ`7cPZ9LclCz8Cp)7;}fugeReai}%&vK}HhAfDj&g3BZ|Ne|hA1<~-NL z{lXB_7Mtpx@@!9_CbYfIo9io~r~AD^h_8C-2do^5$Dd;gAT>k`K@?az?7yFI?WhZ& zZK#p&g;mb~Jexo4^T!DqhIX(Yp5Vc7fL#TL(rS1png4?~ndm*LGJy_^EQrZ;15>cDf_D5?Y zO@)9B?eAm!dX^uVprjoA&i~!N=y#W)G9FX@=c0cIg|~mjtMIf=qfzW{B$eO$u^&GY%-j8 zb*P?e*a{e-2OB0Hd53PStZiH#7dxLACmd{V*`^%jDjlP=x|Dmp8XWc&$5qBnADZm% zbl+IW{zTC07tMSkw$siuzUp!9whCJod$wnx`&vr36J>fg*-s^HhMthrQ1fW<6syz_ zP69fsT;LQ`{X`4dz|sFp8*3?2Dsg~qwAdZ?Kx7`@;q)isq_Q(<#QkA5%N^E~A(td~nu zml=_|`yrz*Tn|T4eP|h14Q#zd)QT=POF2A3q!WMV9l*16+|*4)c^7E~1`Y_wCJ2Kt zt=`SCdE(Sh?(>XWn9Gp)OQ2AEhA2jZ=3fF(_ivPQGXqC> zz=TeZF9x1Qnb|CN>?%^4X-{JIjLLk+$33I7EuX~Jd!c)eVu!h31Rpa5Ds=2Aj&3yA)!}2#Df9L;@D?Bx~u-e;58o z!SC_6Dq=)G@s893&AUBx_0s$EYAhfEt$tA1dye2_+2%L|n1f>EQN{5`y7~38=W{C9 z1fPot{v5DCdrB&2zU2_RJ@EcEfr1=j;o4sa!=WqzdxOF2+R2p{x1Irl`1rcJ>51mj zSF`W1)&r331L z=P7Sqzk7>|O7&ay@>qQP<&`+Lc2haYGlz_p_%w(d$+Pld=DsJ6Ze2Gqu^354z1~yP zPFs>+2Gm!5k?u_xDvwi5+xgT_NQ#9b*6j}AfDiD=TTxtDs z{L+-+8?oqTLh^Wr*2#W-4L<1J2-^IfQX{ZJU1ErKkp`;CCr!xGb^I&c5_vOf;;>m^7*wAr4%f`mu9vFH~U_hX2PyUE%wLQO0jC(=^vxD2%~e zUdc=xHc}}!ubLk|F`BMyI9S_~6|c#Mgu}MW;MOFmVJ|iTtj6l&Gm-|iJhG|2ndAk9 zbvIluHps1MrM_$ej)JDAN%@agReHWVjQii1QplYX(KD~VWS7We!fp9CymJ#;XT#-R z=eDx@E<0M+GUu@7YuoV~q(nt0Q?1QxdWL%0>)}N=lkzft5!JOz*^x?FQ)jm$dTksc z+q(|A2O@2H-jxT|-st(lABY*;3H{r&@Rx1hPP1h{uD4v|v37F2mr{>zcelu6Tku*i z`;)YbEMfC?+ngXjFXPjkjoK*Ch>kYku&TV+lbtXZ-VEMHE1&h=@FqxI6~x>N?5m9V zk+6=mRSA3Vl}M0zGF*>L!xEbb$aa3Ywu@h_Kfd%2^E^WS**U!%t;%~Bb(FH92$lKB zCX5j%HwTg+%kvK|16E(_NQaw*Tug2EsJ9oP<#vAEEc_Zsky@B6NU+{^hX^@HU3brOlt}i?# z_G0O45hdpC9IDW?+vEOO@6^B$sk3b8H(0q{WCG&>a&?CW9BtBuhK9~RWI|Ll_t!^+LW^>y!W~&s@8Hg6b}vQ@k;h4kU$HN* z1YUp***cbK@nPHC#UHI7=FoB4UaS7JlVfVu+R@~ob6K#IGgN*D4w<9A5*>cXSqOAD zk%uYTPV+rP>0-6P%Z?ZmqCnr!-{p-}vw;&8muEGX>hK=j2*||fE?qsHh=TZCRP6XM z1DSB#&&y~-rp3j_KF9B1Pfea&VK!QS<~r4ue3%q#Y}4XPrn}Y~jIaD%kKbOaON@fO zrroBJy#DDdo6UH^Oxd5%+x)j}E>ukvGs2l8Mk3cjo@}0$4dI%veG>X3Au|m#59jYF1BZG~XFl-Zlqk8CkAUtG)5r=H14v zH8lCKq<9--qzc>E>!0n|xSB%LJNh{hNf^T%{(3lPUEO?BYL-YVm54RcRd?z)OI0|V zA1mUAHW6R#_2<45?TlnJRy%i;TzubEpP1hpiW8Lc*MwA#|4IPVpWOvK6@J_tGs6KUGN#4h2PSza_{`g zgmHk|Y}lw6>}~T6RkmlUG79?4fb`-%4dar&IvII&@yVd$Qz~P-v40X&?yx^jS4{Xc$ zb%w{EHpskJEz-e7*vBP??3Hf}SMQ=Y$clD0d=;T?GT{SHiMqr>Ho03DRR~qr5U&yh zOtJ@M2dGKRviP#1A!JEi23_$?h_)9@l+}%N|HXCt1lz&=@}c$W66^n$bdph{WVHnTf8rY$N@cut~n>pWn-Cw0Th{F^9T-T~LA z`45|m2$0U?!SOEtozJNaW zf%f%Ad*4Ml?)EsVmDS|VI2I}e&?{H69WebeOhoT`w<I|ar&D+kTgHbW6Y)DdRyZk#i_En*^2m`)UtR3@kz^1q z0IjQOfB)>1aXotV79nkZK0iVKPeVtsYKCqeIk${3PP}SXcelU_t?S3^qHL5rFx8U{ z(?=vAvDs{GeJyFT*%@!@lMwxyR#p`I)rr2LFZrgKpl&hlU(UkQn11c@Wc$ z!^}QbrbUisnUR6t!h`CnQj3fZm+X$}A0hQ|QYDdPn!08Q8=dW6I9Jh}o|LsOdd3m+ zsroFOAHGv~%m&dTf>yAG;j6;y?_s?e&-|XCLvbgMOj5c(P{3SrwKn4SMSJYEztPlv z> zHh<`LCW_+S{jF2=eU?<4Rz6zF22D)cEm4U#hstz>-pB$e0(GdeEr}TmoZFYVdZ;J* zGvu`g6%J0uLm#JhW@I|eISmb=l1`M zl59fR`K4pR22kKuBb*^Ti$%?sFO#HIpHq>7Yr6Yf|G-$x;yBvUJwrpgla&u6YTEBD zGj6X2x4Jn~KJ~V~VU0ras*mY<7i|6PA&N?`b+KnjN%1%IPV5`5SrBN;4xIrf&RN!-Li-EA$=OvuX?~gBF=~% z0W*(}L93(+YW!8{@mJRYQ&2$|9!zqai7N15WM@yEeN}F-QnY=VYjvL+J73k#gOoYH zNK`w6^)8SViSKAs8~ltzeE3e~(Rh7xHVz?tvq(BY@4psulPCy7NY*ckq#w0@k46Hv zpAw5Ia-4UCPo!7L?1)c)JJF*gG1I>q2^l%oQgFJ%yDd5`zsP#D!^n?eRC^e;iLL9TwrHHL-!t#N z$5^Z?N{P%4I?pPO;n;2>BAerOC$ujVSwjW7-WbPNAB^sE2wDi~zXH5M(CC&@+WH&( zSFs3AiQiF?7tkMPZf|-Ak659StojU7bl8hu^;v&6+!|7U!w{8VBrmI-^(-aZ;d?`Y z&jP$daJVYu?`?dytY)c#*)Mcy$^r2|is4W&!HXHG4jR+Gya&8y##~u8jQ&VZ@ZY%A z^zmJU4OGSutu=cGFaR~?oh5Q@IhF8MR)%Txmc+VG)kRy##}_8t%``~wI|rDb{?^LW zq6pGOUyMng^F9ndV>J&^5)XUtd-IIfY_(3^COafl_|qq>=t5gb-sg0rzA?&Cg$*q( z4qr;j1jSy;3S1^54zbU*WGb`E0|r+tWyCI0(-yD88M<9znoqn*bl?o>t~&Ccz>M)y zit4Uy)6cNRO1Galk@oOnC7I8n+bM>^ODew?@Fi;?Z}oTfPH~0BB9R~rcQc}> zP6^bE&f{s3YaLqt=f)>#hMy6C^Rc`^p@U*hto5u=2{H5iKkpU#xL?X}gpY%I*;v!F^Y<>~azDAfohaz{ zh7}{C)V8T||At5PCo=&9j=pPeX|6N1Y2I#6!T_b+-nBCZlGJe_=<1H6eqTH6vhdup z+%4}0N$NyWGp8c^Ea`@kg)of_0F5W4zx2^vAl05LRHc+$U;&n-|C(uryTp4Of#}MZ zToO;I?jlLSuBZ=3Hm&&Af@6OcEnYpCkKgiCS^FU>F!WLst`JZ%nrUa3+;)DD4bqI6 zk_KgE>LYADHT7ybx~SOUS0O=aLGR;b4`lAwu0MGpc-J@6<+%P&S%Nv#k7F|P)|}NG z=_e@h>FI>H3ij_*hyR^hu-)iX% zM>mI0Kl5B>2|oPMFw@{BksexeUPBvAFnK4GO-wr;O{LP!y1Jm@I9YAsKtm;I2qUBW z?XBp$TVRzy+a2RS)RMt+ zVnMpN)yS{EKOUQDj-V^ISf8Tf)kT~QJbX--&Rt=1pW33FTj(DxV0V}cCk6|T%LYxC zk=Em)kJ2NK3*z=%>leQGU)EJtTSdfBpQ|6BpQcIFFL#W5CpBBk8BjvRic<- zUq0=+g&N=UiNR5my(Q;_^f<)OfqUrXL?4nfv)=pDeo8Hcix9oFUh^^ha*c{$g^dv! zV>YeQ!tpEe#ldPNn2a=-uk;#N@6@k>G-dPfvKdsn>gwXhFCX+>&sI;NAwJCTpz)Ab z#YHskZZr84Wa32Nyw^Wtm>M7Z@nCFMEM)kiDR3(WeEwr|NAB7)$>A5SEfN(uv2O}- z;(ms%BC{nLYj=aWTJz%KO69~b{2wxQL%AE)CdKRL3+y8wxAQ{V<{1!6d!LDxyHvQX zy!W+g^sXv~ly85S$hifC3E6GDw-;2h9FR>Qd;u4lFvYZ`{Sz*Z7o6Q?frq$!?H5*w%pqFvJ@9mRH`=I2Mv zHoq3;scI4#3QC>g!Y+U0wOlW(v@cfiENIQHO}0eu8x`(iezduPQWafJM@Ub;xtyg;)wn4Lg=nIQ%@K`(OR3%H^gw=f?XWd?s_Kw zw9)-(xGwb8uJ6cQ_oFlB>U^iJdz{`2HjI_)I)5q+q7qEqKD+Ai_R8>Q z8C!jubkS^R61}>q`zLHEWt|lluNLUOwsV3m-|9ldo4)&sFXF){0ivi|HbEa`-v6Bz zcCl3?-;gkF#`b!o((9=rZNOmS)&~U|eXjLY)F3~!bR8Bk<=6z$kf#u))5%!O{OjDJ zz_P1|h_EAC3kVnDlyz_Z-OBIJbgVHgjN|7tyFcSoB(iP>uM!_Rhy7+Y5XG{Vk+S;0 z>S9#aKu{9k@?bX%c2HkFKG29EnQW!3Z8woH?sp0A_(q1^leUFln?NuA43O8bjVYtbIqU5d? z$o*246wM=~pxRF`y_|BL?lUT>0oC<~QnJ!a^*6ifh2o@4U)1Ius%<9dcJIAK z$O@%{0h2>itWV83{a_%;N3^}%zOL#ginqSPu1Ea2)FQcv>ARF9OHi4nD5?(7h^7p_)j>?bmeasK>{RlWVFXoCuZ$s`VNKc0kJf;y) z?RW=ij>AZIq&FR05>yDmw3Wr4tJRx0%CnELlvCSgU?CaWCx5w zDTHo8zwoa3ETxd^3cWYcHHylUi&4FP(*7Gm6iu%?P;Yg25)M^Yyh2PwPR$Aj6u*6< z7qO5RuRmMC_})TzlfILRQzIzJ9ci!ZDSk5(`tq^&tz`=_ce;p8M5jlbswA!C)#%y_$^SeVyw_cmYsEyb zu+~Shx|boq3x9d^n{1@7>-(YMf_oU4YHtxJ3%AQc?FbIOeCh>wN{le(2Wq(u4(~`( ztB-odrHwbLVK6gQrP(D>9q&4TjM0Qpi&IL$0{M~zbWjvhqk-z&S7K59zq6X-`Hz>o}xMCU9#M0rTl3xgl5& zdivRk$VYA=5u*_pnrr$L-Ox@-~&`Q)N@7eT)8z8Mef%xpBug3 z7@|s6zM}8y@iH8=N{yy?<&M7LzEy|L-# zq~?CGx-wO`7ez3KkH(FELPruKs;Bp^hM#{cphxfuJuj*h*8AMSyAF}QO<7MIQ+VeC z*m$tlfb6V*Xe*Bq$S>AOmWbtwX+zFEaIw);$kRxT-GPx#Ffk05F?*Y$p-=5cTb7HcF7Az-z|q7YYID(h&AIu|3WuVWJ_Z%CqQaN#FO zl&95>L<)7HsMX&9pK#4`s}Hh$%aLf#c+qcDBJGqvSG5ziVgP+P^eN5AKwRzj%LsN@ z;S5;}gpNg7OmQ3-;ixSrrD_A!IZ=R1I~-O4>8Pe9eh#@CQKY*qYl-3!011TW*l&nv z)!IXHqP+XD0`ftL!(`sVJT;*6!R&>`K8l%xXE-3_AB%m5-Fsem*L(hVZgEuey=(lLM_(uk5uNp}og z|LgC0^{mAkWi8hF*1hNMv-juVYYDztB;}cwGr5#S&R2O>Uy*NEozqzC4X@_iw0h5F zDTh8i0c!&r8zFdUn`kdMg^%%Axip;{I!B68$Q1>}q1cxSw0K=nJRU_fywIp3f()~} zBkZIR0$eiw1=GdNAY?c?Li~AIhK2NYF{8fh#i4cg;`xd?Vw@=!n7#`}oX(~&a31Z@T{8G0mSqr6M0l98!6*o_t!jO4{4(6z9U_G9&STSWh`sP=WAV+Xd!d` z1bf4-kXeOs^w2s^K2$>CuV%Pw{D_JOq*;wi!Je|gQo@*`J*-^P(yrY=hUt|Jp=!Fs z+4h3s03bU1{@0HnAy@D&S_>~6N;%=u&n1gjUC-x`vG#7Tm2J(mL2=OQBIUkqo zGk$%&R&CMn{7=2w*4sljI(nCKM{DlqCmG*%@x>$5qFuz{nLe<(VFm9O>p0Mz4NU-a zD8!;1UR*31BL>; zEG9qZTjtiLpy6*Ah#zvYn2+jY)P~<8WJhuS!s@FQ7@yLEQ3xmTAX`8U@EQycC}fu?Qff=s3U<*?Ls&$P6LD#*_jQy8uItRtWCY)K@YkA2GtZR?Gu14`$}co z4p(D>s&GUTKcxLkN!r86)8hM_1Wy@BOKA5PWApA7+>raj%L&khiCU)4SZe)udzI7l zZ^-jCV8(&?`fY`fz*y`zBUqj_WBKRz{;;Cq0k;XaVpWhgY6gbw05m!3BYMog1}ZJ- zm%(@Bhk$AlJx=Dg91zJm@1tj3~ez3Y#h0+)J(rli3xHH8C zWrX=s4N)~PDsDId1u{1CgC~K}brrvq!lU?3fj;Zjo!_^GN24{7okV)G`Nf(GXkn>y zIPAA1Zrw&d4^(d=MD|Fmfp_WiyF(Hkj8v6^odfqH<#{#M@>1k@Xfe5@Q3%y7VEC_HDc9brb_t#E8^n`>~ zYz8me0hMsMQeEmWq5DcKG;O5vO>&NkP)ogKP->6Rq(3}6ESyR zg`!v*vlXE@$y=gptr4{vDE!H84N#@>qQBcK(xg@dT(4gYe9O508R(j2AW+gMFo)-E zJsQK!HD_HHyt3cE=0=iLhyAJ6o1C`FYj9lJ2wHs)W?~|nFF5o1k0kCmEV~*jYyUe# zy{IAJ9fpiBum<`u(V|F8gcSz~-C-1`!gw|&L?I@0ky%Eql$)?Aak{t(+BPzc?e!Np zV(AdY7%2bd7VIk(Dt=gP9;ph!m38r}0^jw?R@ixk>B+L!#7%^1>|5xB#Ss|IIO1oj zztS!i%hGz#$CpsYk}YwzKq%ON3X;9KJj~0we)26^*;Vqvm$IgcCjc7eE7H1r;@bRo z*gy7VR3Qy@*UhszM9t>(=92$u_s*-qrl&Znzurux%rRddl<8Mqey3_#?gl9T9b9krzXI{*h5>{3>E%12JbQGr{naQ7y3*(Vj~+ zKWh6*EHrW!jm3kvx7shC@7?WmZguHKNZmF&ZRogFfA!=lKD44Z@*J~nWLUczLgqa- zY>e6UZfJe=ZT~pMD=)AyeZZm}+xKBa&+)1Z2{_)uY|^p1?*p=y z^(7%zfljm)3KN32@#N?1I^ve2I9_kEHVufTYOTT8ryNK8v8J2<9aoXmlT6WQE481d zBUEK^5E|p#IoOsD>s}K*4k|1>cvPqh-v;ta=<|7O50=!Kb}*%=HUo#Ar|vJQ!w&xB zHI`*R=T^*!DwFfK2ofO_{Q`Ro;m;gW9(_4QN0B2v>ljvQbx401?whCP4Mvi}akx+* zpl-I3OCG~LtFa<)ngPUH9G4UxWh0v<>~+sTrI?2ApDW<$ zsTDu#ol5T{)7Ya+`h-K#%5}*EBhiJsM~!j$?{Zf8+T(fh(bQ|dkUx@gBcHo0a@k8_ zm@-Gw1r0L+mHm~?L2%+QVvnnm`>&Xu)7t`ZVghXggPPu?&~GiOtq0LuMp*6FbzY0_ zo_E;p+`HVFkxmeQbwxrSpuAUQ0+gMwmYiT}V8Hz<9y#fJwOrBc<|O@ixC_*PHJ(M| z%C7Vp%J_3`i4(ouYl79zXX@?OgOJ?oVyII`r}!?T+yKK6Y9$VS`Xflb={z&zSG=?{BnS|L-_SWK)b~VpsC3@%kRt>#m$W^Zhi~4p?!n{Aan-gwU)K71v zY96P#w#~n~p_aom5(`_kkSQ>}o+J%l(1Jn-LSR`m3n<~!Xi*3DLK>z()_^-t)Vfb& z6fnd|l2frGf`7o2Mo>A7`)u@QyMP@%LG8|;+I)tDElh%g!t?ITBL?a|{m)ym8Ls^X z+iF$>PFE4$OUJy}r@u4da<%m;=Ly+(fohDj_d_l!m*yS(>72g z^^$2E(j{RLb2a@A!{4j6YNDUKsYXL}v$B1JLUg5+^Z6PT#0M+_KU{E63JEE@(U(7q zlG~obbN^j)8d|SgO)u^Gr0V|DjN{YWkIp?g8B-eeN3w7-f6&{vSB1f{pR~6mEgqO4 z;Uy*-3km0pbQ@n27+p=L7S9>0-vujnQ4|!7HeJ<`Vvuii)9|57L zG9nKNj2C5$sLembB!`B6CHk>!ygdNIX!)#FG zayDc`E@6wi1Dac{B#i2%OzFU+k59pAiT3)teg~IJ*w@Bs~PTOjNEw{PD@`vEh zt&$gF9)5>)Oq-4GG@v^ZyRQRPr5%?KQnf=LO*=N~xD1%nmH#l$^vG?$zDSNvVPSS3 z6p}M-nw;E?&_!Tp;T*Q=em>ZAyVWmgE%5o{`wAL{llFsyS6AkZc)@4CxMf7X$Ie<& z?Hy&k#VvbvHj87X`B>&~Fl|t0WyPh;ku?dX!*^KsX@zX*R9yF|>{a(=U{+TMP6S&x z-NzQT0d?YF(9j6bp+@@If|Yj&%hmMPPDrO$_%aTkSlBZr_)) zE`Okad6EBqC0v{}E`S|3(|s*mjU_6FfI`w#PmgJ>XrVG&1K?T12+rRDf#^uaOs|{c#&t!k!6NvGlCKPA;-E)iIdf#M6H*>$kEv@u!WQ+vG9A*s1k ze3}m5tyt(feQ2+cU^GjL1Gmj9YyN}*oYzAx{6v;iL(&lkgnZYlo7f2bZ% zO{QYaSZn_1Ci%S%1{-LhqCGl7@B!~StWkJI)mo{bP?^1jM)+-3obKni=ypQ|1HJF2 zWgRMlIsh`Fjpg2AMO0Pv0D@a+wAAMxU8@v`KQpJF{p@Ptme3P7O_Pd;$sg72+K{=a^!;e(>XBH-f&PQl~8-GIwf*o!fDR`MxgZ`Px?cd!2mZ3>`Rd81hJjyMJ0pm>mp?Wt zgn$cV6(TWnwibOyJzC$+wmpnkkM_N=E1OG({!O3GTDK|-UbY_lcFuLY5`8?DL~^WC zdswNNA*#F^23vz3Oge&<`t#MxTA)U-2~>GMfJ*-|}ip z`eeiVmm)J|J!C}5T$RC*a4fGWO`Hpw3o34>2_Z_|c+~Ni)H?@-~INhH4yJ?XYx8Q)v<)e%|S2~Hh{Q#6s8<~ zMqg$64y(WVJ3}2r_D48YUS;z-VU-mA=FDyAN=l&^vAWcqGfF!%3+Q7_g?~5u@)zR! z!to2=&clyKd3$ed&S`ApAU3y<929E20BMICV{e7T>P+XZ$-@t$?~Q?|=!WaOqkT+< z*F;Sb*NWbHijpt0qvAWJzoQ-@4P zHoAWqdXfNqFEu&RrbpX$Y4xMq)J`4wuec48ZDE)UAaTvU#x@D|Fu4zqxcqfnq-zlT zNoMSvLC&r05y#~GGG%=CkWRMecStBXh=204nD06>iKZGtRmY&uTq#|@Q)A1fVs%N7 zr&GOxi$s+$2V&o{Svg(PBOJI|OxufE~7voTb;O8mQ z-AD1|Zlg>4i6*OaIAt2#N2G^M-?hO#-BktQCXd)i=WPohW8_1SNIT*_rXh$>(y?}f z2zoi%`+OVlb0c5ZQ@8Q9^J(`d6^Q@H=TPT#L&F#^IHd$Ynq?ioihl5~JxZDb9o9%W zeyUkC@HVA1!TMsm93t(DUqp!`053G7wC*7c&C?0BGQfnqUw<Z64%($RcCzHRro?U)24V^uzucBSFmwu>HKJo>YuR2dRIgzYoGt5nW`! z70f(61iKtn7wR4U!sbRMO8HC;yy%s!r=)J*VLI}Zsg*Bh7(Y)lOX2G%u#qZkyOXKn zZW!Xd5y;D5%8+_5+)WnCg|ZVTsCSRVmc<1E$+S@DUy9@}SlWN$Zbz+Xa(W*517LB=Kd!3WU#N<*VIg1)e3(NkJ;6D@p{>->5W2ZUBF>to#KKwiYB?8V7-x6z2y%L^NcI#Q+rw_+=f3VjW;QLWA^p3MO)-NFri4&P7i*z|WYEXw zmtP}&7|hNQzEjBwW_NFQ7+{0B2z2W%WkIW3Xf zkVYTGwzWYfGEEV+nDs;bUq@&BXi_Ac)Ca=wR#?UQs}fJTuOTrWR<1@M7l9wyD6Ue8 z(o@5@4|pmTu~A=1Q!RDSJ@YQfrhj7h@lo_k&(6~zY1Jsw#)e~UpVw5_4rcuML4vGt zQ}Z8$JdXeM_h$?CdtG_aR2`-xVX{5AiCZ?%*_tbkv&NOdTq5Fd^9PTvo>I@-gD3}v z`$o&09(uhOx~dv#o4;UlK@#GcgxWS9S+?jDB(S8xmy!Vu79JMC!W{)of{;b*$xiOF z0)b=`DJXv%vq7_q>2L^E(fK`LIcy9Pu!9)?qLUZ`bmk#IR_Rz49ieEum0f~G5ps2p z%hL~O7XR(}{6l&oYbmB7HexOq#ZWPk2y2ifWYTdJH8CJtW&T3Zr+41qX}@VH_+0D> zPp;0~25tN$!NH%VO?o(c(&AywMbl;w#{?;`SXC=pU}x%hxsAT8a0cdUMAm*M$|>j05{Wl=2oEX_)^uU%!34 zeZNARaI=CN-C>~Db-r;F{P*ay#gAQeX+ZtF3OergpM8|S`G8>0aK9B{HGmOPp@FI} zp?hw}=@UemkWhYaNx}&m)mxIyz-o=cQ)qeEOBn;D3UPP3YduN$U69MncB!uS%?|b{ zT!DZR)D)(mrdWsr>Lkk~teKZ=@XA_fv?){K8pR z^bvU(H|z*}sR6NzxqTtl19&6<1Rz4_4m&Q1-d8Cj5`K^9SO$b1mi<6TG(S_i@aPyv z)JI1oc6vy_7VLu*lkitQ)@609k0W$VU&Ep+?povbE=b}8OlJR3{h{qybVBZ$wk~yK zr*~B}XwfD4;_j+E#X6yIa)1+pyfJ)tX!MKTBPEeahQ_rE$V^ja%UvNgl+i41x*Rgl zgO`{f0V4DgOwDXkm^$bbPki~aPjmjmSdYCeSQ^~{8oZ&~VsDCSUOfKl-cxc3YfWWT zXs@+kxN7YAJ@?tw><6q(c&r;5*>Pd}_+3=6ef}8X>2Pa(E(IaRbMOq(@urt2+Sr>6 zQSDuz;WlYgy5EvUcI^Kcd{?;Wg7bV2YR~NgmxYDT>P-zcVuX9mgn4gRTn@)?##vsT@AF3z4_6@anWj{^}c4t zuRN~aQR>0%{%V{%4FWroU_x8@iPjR&BOn0uW7Aw7f*=GJa9=K?MV}qiM^voM?YjGi z3tQ#QtMK2@r_vWSCuc)(gB-T7tkxcV%%*W1u3fwN*S1TvLr^r7#6Q0eZ)B9$iUZWsw(L6MPNqpmCZg(u8VZ49p zJ2IcUP4>@SpI$Uy@Qd#KO7pu{=4eJK<@X)qVchz3!_T)%=7{g|1zrc8Hv~mq=?4T- zGS|@lRie<2{W$%D@H&r>Ie5{~gatSK$t~R3fHX2@n1pB}n}f{AdnrQEU7nWQ*B-zQ z&~HSRbaMBV>g-Nbdiy-F)h(E_g)yG{#YC`Mj>MGSLMQp`t~gJq@El*-g7+LwsZ{^& zdEfG4tWe>{PI(3>*HW6lHJeLp%UmVpJQh{+LRouD)H;GTJ-{6$f&~JTk8R(R&9~9q zFkBg4n+m>&)1#ihzpsmI#I&9w2Kh?Kky+vcAg^bb7AoiUR_|syiWUp59X^mpt4kL3 zg_wr9s@e`CL`hBvIfaXBZB6N*|=di@QB78$0Z4{a>@!*R@Pk1^scI~$FJzw+j=Z}k)_qj`6?fn#cD0A7H&Ln-fw)z=v zVxvPuEbvwbyI}`e?C0NnzsnDc#~}?C+~kk%EynoD;JYSCa~WuH0F|cQ>iIE(;9)UO`s0BLdUx^qtubp0%u+0={a!c+)S( zhhW7QbJOhqw`nx9-uhT({cC#I0c-ZP)p5ouv-I!B97ERkVT_cAsZMYIiAJY}X1#BL z6Rf&u<8pOgUTyg*C>&Du{9Q@V&1v*4%d?dJGsANaZxWWbtye$x(vl%#cr!oE?*ENn ze;WLnd^p%YLYle=G@kVW8aKMia4+NP3x*GtL{b=TrP2PegT8b-&Qe0HtNNVNEXF_< ziIGKQYFyY~@8HH*-^*1^2ID>BqwXqHdA_z6tO!(9xc~C|KoEY^4nB$mqj$3cANHr4 z2#OW~mWTjn!mY}OXlr+#PgeHez~yXbG?TlRkAs`#hOM*3XC~*eD111Yo9$-|3>N{s z<*Rv*OJ(IRPGz5T60XQS%Y9__WymEME^6bv$u`1CUKL2>m#=+-=iZa@4{KCMGJT%T zgBDrUxtTqi8-rOSgBEK2n^>hhEL>{kWHiyk$EfvHJqh6(x93u@^6FJ3fOvhkqejk& zR+*5fB1QJe<~&DI&jaKCx#W)!;s+?*A=?$fbj?OK%6U#pzz6@Oi&IKws*s}AY7!xw5r^ud&i7@&xBfK{!lPu%WCabrNH>_MwCE& zYoFXf6VB$A&04iU@|vTr@SCN+Esgu*JM0O-P>_Q-3 z;9y8)$CV*?cuKEy$OQE!g-aETG)BDTJImQo&K%EQfG0@wFlylG-F!&@Pwqk_BPlP{ zJzpChRn`$R9mw&k>e%Qp+FF5o8U6*bP*P|&UCuQpz`5ll<~M$Hy0lZxrRQ0AfBvY$&*G{9mlP|Xl@M=g8SC7d zC*S*BDQ@SJ<@u+eGbG*LfRquh=#gPE2%QZ*L`7GP*mI*DB|*8`N#r(LRl3hONrj}> zwT?Q%kF@t*8b^=pj0KWx5xCnknh*#l@1-Io8Hm^tv3+8x!0gGwSmHD;*E@Y6+KFP_ zWQC_sAhq5?r>T%Q2j1lT=$V(UaWcI0pIGUuV&km4|FnrD20figzD^N=;=F6euiT`X z_xZ{knv>!G>MiSx=D0$0JhVyS)*}Q=OAGRlP1htp-<|C{Fw#)PqXl~JD$vzqrrjtN zWrWW)e?2o2qFpu~@l8x(c12I)YfR-zuYRe}R-A4m{o{WIjrp`(hW(w+(gCAf-%IA* z2PSY)JLi42k-Owovc%^F+LLYt@*Ij(4z6>-SMlxi5~t#4s%60-&*}cCb$du&lyPWS z$c$s6_xyoL)AcN_>uRI}m_uD)?j6jl{p-4h*x2&QC+9$7J9%zaA6ziE z7+pqZWhGFnOYJ59r4lE)9HhDua*;D5ulPVT!8o6XOWator!e~q>pI0M9MAK|7%dN`0a#$TUxVv{AIO%;`%jn z;qv&*^<==ifc&7CT{KYL=1TSRno4Xu{v>N(T=@*Q>1GQj;l3B!`Adl;|L9Qtt=+0Pa6Ift1ZGA$)nkm!n9;>5qefHvC-LAq% zJ)e2Lh@aAPfP;l|*=dm|xc}e zueS1!d~GXf#`_+Ta~|p)*V~4r3b=D4=BGOres9FvPSpeDEzhChW~bvUF2SF#SU=JE zjUn6aIJfr;PStwPuo%bAQrm!s76F01U>@3V->}Wqz_xEYTSeuNEkE+P;89Crj z5WQnb$a7v^LzpA(Kf_>kP_O2AUoh402+a=Wq7k%h>)xS!PHRR8-Ne_pZI}bkVJXTl z#LGo$^1VVz{oCF|Gl-VR@x03t-PzqYa=iuPQb7dYhv)QJisZ>;)OPo{uVxazR6%xy zrVslOB>AbA>c!H@x5wt#qnFip8&@lym{&NxpHC}X&Tap#>P7_j*^Wq^uym_jKQCgj zyxj438$EBonZWn)=hpke=^R|g=2Dj-R9?O81BR)Y zeesB)DIvhrxc^ib1chX-HZ?0dxUzYgy1_PRZv*7D6dVvRVvL8iP9O^_X`(H4l6p`s z$r-<6H}O$09^gCi2w8271AT9OFazzS1-rWpBiMnyc5p~{6_q8_H&T!%w+CljdIJ6I z*0gSKTSg_Erp+(l$M$afNwzORwaWx~JsbJRo7y}$mveAv#Txlb%e5a-RP(Xt*Fw~) zwU-M|vo3uygzawA(2(0src>V=5X*Y&o^yxCijcwL=SlDwC~tJt34`4iZitu()8qoL zKNP-HSA#KL3cl4eQum0L`}vshjQh@}_ZbNzCx|Jsq606kH?emj|8-EAlR!r;BJ$iy zhXDZI62)%poT&Xo#EAVJFaI*k9U3{v#y^YygbPSB>+hdTq}pzmHwvZvcKv-&Ogc|M z{a-2Bo?u#{H&T2Rr1BBW@gW(2T`q|jz0y5W3Kn&nF{Zp{f*PD#EPJhD zRjVRzM-|CHJ#X%MrM#DJg~2J4WcxISv8x)l960GSLEQvwvV`Jdlq#$2F6jI0qhaMi z$Jh<)?x`;=y%==j31SbaTNEoBy)LB4SZ4amjyp;@KG{J_ZWF+q+iJPfx>;J7xY3sn zIx-W~sOL%8(Dfu26!PITVXSR0teBQF>*YKa5z z^qBQ!V+#O6zxR<$TP~}#Y||GD_oigQ9ZL0vGjDJevA(Ljt=iI>%Q>svy=I`kKdHV` zB!(ftH9x8Iq(sOYxa{qP;jS6qvCsSDlNLCMw-+_)y=i>?o%OyIY&B) z(|RqGG-$i`ecZ@}G4b5}zIQB(Ff`1qRTzAenNp~p0AO&BYTMLfK9tlm^X1jf+nI?t z^nNM9FyQK`qmDc(ix7!9A5ELKEYBZZ-8juaMOfqWL}P-A**0QRS!#Icpv*@}nUue3 z+lobhChrgVYwKo{Fu#x`4&g_)$R?((P_9u25@6Qup1e+U`F(($_@x3<2q{Zr^20(< zY#uJ}j#UQ4uO(D6;B2UK>oAW^8>;>dH2uXq{(=MjBjhZG^<4u?U{lg7Y`We4N?C2E z&zs{_AWd#mDqCo)-d}AL-?9QaISDU+K6rN5`%``?6kY!gz2Va zJ(X^FomlXn4MT`dD^c(aV@Z*)0Y#Z7}3 zjS(r9^&k+9Y`^KH$)z9o?ym|3=g=dkcBKCk=(QQ1 zaKC$dP#TE%bu4&&ne{o9^>*(`EmdIP%soDR5`_6VHxmajLUBRISrhID$#Pyc&7u;?}J5mM$Qu(Qo=SScayb}8~@XLnB z;rqvgcC2bPzcvE`!zOfoOayS*E{TEe8Oz!G42qIfYO2)~>DG%`^GiPc!vuemFFYcf z7bj!id_#LV>;-j&nW@T z=*!ircAS--xal~ zXk8qsh{KNTQVgE|t*Y1@!vfaLKiK43;>8f}lq>R;;F$}|S?R{PZ0-qt+~)cFCEEu{ zWo_^c!IAN0Zh4CD|NPC=V3Z^6OHI4>FrB|v2KtcBc9^Il)}3S$R(=vC@4$Z*uZ}8| z{Vkj)moUouKFDVO^Bw$!{r>${X!sSiIGpRJp}4mayWGX4*BKOKA)uesYj;2SKK7G8 z-%~vlF&b<5<&psZyq+t8KL9IVaT5>vYPXnVrM{AH|_NB(~AEQ`gde6QR0NC1Vg zGa-9ijH`ZYRTBy1?^NR0w2uya;{ghsfY?+T{6%1QED^^pxjdaa#1dMfMNjz{f%+Ba z`bV##|DeW1=8B=ON??aW{YF)`I|~jzto7(e3qBqgA5LB{KCTGYN8v#|J898R&XLPI`6jXTo;qQ!Fhw4%(pPQhzz2v$WkZVFX>T?VqKHDVvO=aP1AQGz}&EC@n z-dXRAB_=Ii9?U2^;_Vrd&L3s96;1?Ts-qbx#MclYNvXNgd!QwD#gI+Oq&7_K>!;0z zz6yto<@vatQ>6a7=wfwB0nH!G^`NZ0FEQTU;w_rY?7<%wfmhx0AjPKYMJ9QsD^C2B zQ#eA{^|!-x(8Z80KiY4o?k&ie3Tnw*X??4^qShmjy zHZ<2?$GGJlYf!noC}FSqEm6J?qXYh@f@yY-2Aa{7ki9u@u;2szdGIp?f*e&G{8bkZ zptwB`44W18U|qx^AQ{R4oi)c4U!tCa7_=3pd@ADhQ~+nX>2WuMRqXA2Q~O=C-dw2b zI{8c6wOK)a^bu^F&d%0voU0k2=+EBrqEjy)J z-5yXkn}fuD@;Q0M$-RsZZk1QhwvuR+OToyezM#L@lRfzTADCU+;s1i2^G$MGqGsji zZqi)*cn^8G8*9-oE!GT%cCAHTi~%2`Piu#;>CBpx2ehwumw(J4dK%8f?HY z^}xySd&ToxiRa(-&kyavxhsEXcIhO}GTgBzzYb}D zHNkO8$l6Av$1zrC-bz9r$0itf1t(g`GF-Yy>e`Vq2B9I~8~ zbZ7JMtzZtDye&BE^tYuu-ZyEO-|sA>(${5h9VeAOO!M1*jq3SR%N3F}Lul%{nF6d8 z`+6ZyU+aZDJ_V2u;wKBWkM4OuOVl~Q{~%7jEHgZ7Wx@f1?jD-!hnZDM0>r;mp=?vM z&nW*0r(^FajyyBr*uQwM%WY&pvBQHez}mC~l~6g4$7?PKJ7C~hrw#2 zC;c6c!)=hXhg*ed^!G4o6c!TSqKxu?4SzP@;vKn>2)KMWhE@z9z=U9(JFosbzuRB; zEY?^u7W_8K2XBUPloWPm`6R(!_2e#=Q56MDhfSZev!<^VW8CW>j~`3=;a;%^fv1Lh zjVAyX3|jM{iM~x41vGOJkFOEM1zZb#>7X*T+tM_Y{ zA4KW@rF7uNB>pN@aGPnw2Tgs=Nt*e-Oqxo!y;Ui=B4jPlX|e09%29ebi) zlR*5kW^z+mINeO;h^4|ESDDApDgG5aPQW$G2!~MM%Q(NS$|l%4iPO1x@O6>)EgnJD zfS2oR>{l)|<>5L6tkV}pdP?;khhQYqQ{ho^Lk}SyIJAVsKri{IN2G4;b?AW>UdN$T z$+|Q<`VldJD;YEWb|!fl7z+*^j)UjGL3<#gmW$o`>?okN?4 z_~%^-pxSrcM*6d2^wX;3mE~zPpd~QHZb0(+6N~(y{bZEhgRDH_v6pE%SyALz<$`LJ zmc02s$?!L@)M9Wh#pY`OdZT4yBhTuEk-!V;cIQ7**1GSgZ-|K`QH5(8E`GVF(yo$+%X0hG4(*D zp?QX7j1p=)Wd|E+8_Vli)+B54KljP!)5i*UXBuHT?=5=MI@z1Yvc!JSQ*erH+xEo2 zlNa^v;|O?O{>Uk9w7FR9nHhM=mv5sVd2!NQ@8RC=0)}$~pJD@ZOJk+PCfI;{DIdBc z{>|qrJ%Ep<2a?fwD!?lR+Kfw3MYYB z8`}9N_w3WIxbUkuKxAMz2w%@MI{&H(<_wm@hltKEbF=Gn5(bZma`&~u z&hO3y~zKn^I500b>IrF(19l;{Zm&J09}p7xcok@`7Hj2h5#zDsI-#A zB$hFGLO*|j zo38!EwAUa2&`wO06(nc(bXtE0^cy_4^2E3#IT)q|j?;vzfnVlA4vVWCd{J)qRUzuXV8n%C*v5Ur`?up_O+I;LPdq3UWv>NFNgsz{_to z3({V3aa=|Rw}suh`UUE*lgIabN0m@nJash!UH330ii{qEy0103&hBeqlwU!{a038x z@r<`+gx=$UD6ebPW1%Nb@@JaCeobUZD}ATwEtjM>`{1w1KYPn#?#lUrwRvwfjA{W~ z`lb7*UYTpD3crv4%TqqQ#8*Tqc}j0*g0>85AYFl}1B-Ln(Sx7*q@B*kt%D22_|eJY zj}|X(YXZathS@BUS;xa)V)vY%ZPF3GH6Wg(Ep%Tt;Ci1$pM^o!#nbd zykZ{)T!q8AHjuLz?64uMCskuUvrc<06C=w1GHEs;tr-Lo8gJ(#(BNW}H%a7XG2C$w z3IMCfixtVnzu{jSG?y1nY{P7Y-bTYM2%LUxoLOOU=BIkE-WE;m+JHxy+=rdgNZT?=NW!ZePr*yS?~y8`>tP zh02JVdJ*4UnA^D|z&tTEp5X>{N&)Z`4)@<)yKCf=+3c7L)qeZ& z7Hskf0c%3*fT*I?^+2<`E-Q%Tx5rUJ^Yv$r;4fgr<<464MkIHfFpg^QVkqaKFTb<2 zArq*xN?Heq%08XXv}2~6ZtaK^x!nP)ffmZqv(&n+{F%6>Pm_zxcDz z?$gW$O}dC5I#*F6y`)6(gbl57ZsvR2_rBdeVtRQo=zqLyn7QEI^{*q_E7G+g=5}NB zPfMqI^mFU#iX~!CIZ&u=ZGDy5$+3)N*j=#Lvm0h%gb=lUohenaTsPy!%k#VQI}u#s zz|(=imx((I$D*jhKvl?V&p?t}-c0lS)5JpschQBKk9BLef*JdQy-)wt(@=gq{DB3% zFRuSXT$7z8$o9DGoleW$?HE9`=s-ZY5`zD9ru7!e8+%v1$<=tA!H@RB?dQ@9N=IxT zOJBECI+PME64DIXrfz#raf04aeyPLrN3ej!hnr&UEt$;c*C!W0#UE=xxt?ukt0x4Z z^0X8DfS68Ay}cM22EkcVoLlJ#gw)6f7o+EDw_IH_Y{Z0>DgOBm{GJaTwIuflf+?V8{F=~T139??8DeU}A+iOYtS4Qwy zz&N>I_zJzo|GK((psTCzNBpGSltB0j>O+x(a~zAcHKH;vVfyTrOG*lw z>WE>GO{l`e7T(ZicMFME(j@RsOJSPR`R3WdCMoP7`Md?aOnMRWx`4D z```~rY2HhtjB4|Y;m~=p@^M4qn<~B&G&! zP)f?d9`}Y#CUYbCi&)Dg)fN9Yq?->;r=n%zzVP0mH)^5Jgox1H#rCc`K@`2>HQHd& zR)qK~Q%F|49@ZU0#eoAIG&$dOS}-)wD7p zwr6;6Xughf@|c3t^k{>a2o6sR$@-KK>y55S@o5v zM(bZ|kq0zmCrpreB`}gl3jb1~XLmTzfE3|K%yM)vO{|70W2iT%Mb+ki_p$SzHmw|3}2%He#2`V$lN5(y5fJxHZ3%^<1)BiRPHLw8XV?c-zn@RxwEH6Wg+Vr9q?nPBhY~D6s{W$Kcu+qi*=Q;Y3 zZJ*~7U+(@vV+EW3as_*s^zPs>`zx|}b_#x0ch3SWic-$T7xN~>a%V36VVbs%CwQyMomgeGzS`tBvw?fbiJ;PpVA+|X6nny5^5)}kU%!Ox8MOL z|Kdehvb9DyZ>6Eo`I1zVZiNtK6{vGd$7B)8A?))}?qzvM)wb$BoOPD(#clMsZh*xf zYzqJ}a*Kh5@!=at(vYH^-HRvYH7i55#r^-WxjgA7rAeyHDME_u483K@Q_y0A=4bE! zmDTX{C+TK5+1OVLxWoNis+Alpf6QC5nP>&Wi}SDrQbSf|J%A04hf~l1WudbCs*oDC zJpApkzWF2FoJR`yI8{;V3nW-JNo$TI02w!KZ;|_LtQ|*$)<5)fBK;rx!N72Yy5Kpv z=|-jriWGS+7DYoUJp{#FsV(v~URy^Tk}mZ^D5S)a)GeR9?M^R4iEhE@q?lS-cI?h+ z(Zo`UuDa=vzzs#z6E&z+Ch=Y$UQ2y0F5|c;m}dW)1K3;k)gd_=qf8sxQ8CbNc=aPZ zf;Z?lHBbDu>mz5t4vWVC!{bJ!xCkF3d6amxd>05vKk&yryVveJI)vLPwUYq2fgMSKGKj>Wt5=q`5?2FF?Q#+(yzLeBjJFf(6d3Oiw~&vj`-RP+W6wEg&bD z_2ag+*)d%8!7Ltpi7-<;FdYHeqoty7PDC~2BCYe8ROYVOV;p+f<9O0FC43243aL-p zliv#tITXL69xTGJ@!U6P-qexuyg~`&mwdeA{3ZO(R%r2WEH#+4h68k{C!|Y}5T_PG zdkhP&AO&Wq4kT?6|B`zaceB-ha)I!ryO=#&4c`4=U;24t)7DoJ3J2a0 zoUlaZ6*PS5zKT=TV6Yl$P&^Eqlt+Lar6hcmR6u=L0ef9dxU}xMSJRxfYYWj|W0VxP zhGlW7}>I#)egSP=we%<^5#A z$JDUD<|?&^sRSEOMx9LE$&vB5@3#o;75gKPcW!DLbnu0tARm%+k~?8V=YWxvBcneD z_j|bDhNI41LGgLWy(M-n+K2U|??dJ4BKoGE5eibMzuh^)2 zEW_#2TcI=Dj9W*Ow?fddgmQ{MQFuS%Np#*7b&$@Jy3apUs38)*`w$ENR~OE25|*f5 z=A=(!9sT=O@2M6aJQJ z7l%qYxBk1EwESdv#nc(Ltx=mcukPYJjUAY@y41sY5a_LQ)6}#%2IA=oUu>X(hmfOw z9JW^SVSnOA;76G?xQ}+NL0{`)GuELpOFljLm(kc?5EA4*B4%t@=oNg%f&gQGSZc5G!@E50Yz72@&YY=K z<8Lkbzd@)mJV=cyEHzg~S~W33q7dj)aIQAgjl;^SHpS|y)0bE}k#fH@ekIFlgVQUs zq59}nhUZ;4A1hz8La!c$(3pRtJhMU65SatfT09o5Ar30^=*J*>L4&~o0vkiNMEsNl zzBVi-l~GM_Q)kTbYa^pFAes;Ne}xt3uMxWMQC48l7^AS=v(E(RLBWyGX|WkvJXQP-X}FypG+I? zn{e(w_o8v%PTRBKB}gj2tuSYya9-1)@jenX=|9iag23-&cQiVg(82Y8Je_4!)ZP2_ zr|1spZbU)^1gViO5d}ed6cCU`I%g|Rl>UNY9Yx~Fgb#;ow(KXQnJtZ(CQm$|mCob}BgOR)a28mCJu^R-i+ z%I)HAG)9xe7|Pu|A>XHb0JLNdpGiMmZe<|&1^`1=fL+hf49fu36%G|I#5`w+Dl07L zj_x73Hb&6vN4tgNh=|jb6?drbk`GDtZTZ6+TjGbI~Ae|{`few}w2NB)yidJ;A$ z*Ys@A9?Pf>&s1{TDiOulh~Wzqk~beQgKHW!C8*$0Vfr;=|2%0`xqilL-Eb7*4WnGT21MEQ9U4V3%gqtxlwbxLO5uJ~;Vg9VSenq$ z4RLB{O}};1ng=9$BVPxO3HYGpr9bBlVYYX-`~IcdTey5re>N<7_LQ$ou*BpiIcZ9} z38>jt3^8Hh2a^D)5gkMFhKK&a59~lV^6tS$-bcg6rU4%Xxb?>J&>!0vWvX7xxblR~ z3PbSQAu`YX;2)M6=Wt=^?{(p;(qhnJ@^9Y^#5k3%hZ#|%1zuxSNXRhw!|!QWc4c=)Hv$=HC5C zf~i`uD)~@aQ-iRwSHsq+LpwrQr{7;DF$;=ohk@Vg&}) zv+T=A3kp0=nCX4&?>ZzVIUwH0`^=X17?dm!CZU|-d=SRpTvjD1cJkI7U8SXHgH33Z+&wvhezY(T z>@<-VLyHkqg_OS)MxgKbV%N@K!~Rc|_`2!lJ8T z#C58)0Sts(yAjtwf0a9hQ7f5^J|kQ%y%BnpB_hM%}C^wV&*PfyMjo zwtv+~0B=8I<HSTQUOwh^%5jj6#%2sGbmF70d{1J@s)k?OfScU4hA#7|A)HMY8`x3uk z5UrUsWiAxA45t{cSP>zv&7G)j&X_FV(sA6^{~-%@ zBBG9!r6CwmVa2+xnma&E(E{f2~e^pjlwKwh_Yr}*wVaJQ5`|)^y zhE@8?Z}!Z69;fmXpt3le{E-juv;U-&;Gm8|XDmeHu(;A(aBL{y#C}Te95s-R&OS(0 zUSjPrgOZ8T+4Vs)k1b#g>2Qxnn3a+KMefRL@aQO#H&~yhIXXD4WeyJI_A%Y`9Q&MV>WHU>Upfs$ zTl`X2ViHHW^gq#8zQ(L%DWCDO%)ccZIb6*EC{2Tp=mKB#oDDqEHWJ*bcPx~h!CU6` zJF?2N8SiV?f`VUf_{PZr9xt5{wCBR9Q^{BBp`#aGW3}h*r#T z^zM6bw|)#h(GCeZQ5+Xr%*5a<`AnlLXPkV2iD4cbI5( zvcR_c7wGH^G?6dJW{zUxG^ z2T^EfJO!y~+PElHY^qplL_TOy;k!XePv7a|$C8OqNEK9SN9zxd#t39X5oBtg^9~5&Y$iO4~v}E3nS5mUx{UqABfCxm&*bw|! z08Ido*sFVCyF0`NMg&+etn@{OX#G_zVFa@N;3J-q;S4|kvGlVAvWL((aFJ6jMe?XY1KVahk;GqMCmBRJ8h z@-sWtmlU150vz9M!6w#HtoL zdqmvH-R9!2d>?!#XwOo5);{$*n}+BHcaok5_tnJZ(AU(syAP@F#n<~m2}*k$%}E)+ z`r~q&8O3jc8YzU4e^B~!W~2ifkr-pVf(8)W&Rpvf9^@l`&3=x~Vh^%EC9GDP;w;Xw zktGo`Bi0TMW-zHnoJPR>9uRueATkt4|4i_f66y(3nmz?}C1jhs+dAu;c1nSF3N=eT zCXOT$bk2wm!K7Q1T==QCHgRHG5eIj6>^W-5Cus^oqQJ9n=>f9pC!sJkmX2vBK?3q^ z^$zS2JiHg;`lXubuifgCqa1uMmk}r#A6`EPyza}>TR#X_6v+F}5Y9&7@hov=_<90n z*e0I-w^xr9_iK8|!UeJZi}Fd;AnRS`)`|EeVcKG zzl3Vwv5bd+r^kCpieZIN`C}VIU4vV-v%rVLzI1uxpdG*o#A&tAB<18kl_aVR7A);K zm8-a`jrGqcd5!z?f=dFr$c%$>5HqM|+$NG6GW{De`g8Zp zy3L#bhpE+`AyVfR>|s!WPXQIoB#8~BU>@x$)8C{Ei}3Bj)TYR|)7t~mqF}$+L-_Zf zNoB{>{WdJvuSAnuM47jlOrcomI;5s%r;zbYgXtJKN9MWsltI!%;1=Ym)>oF#;%#j~%geGrD&w>1O&dPgFkPb{J9kCN2>2)hFaE zIc+jtD42L!`UGvg=gg!I9*sD{|3sSjiqr(>`0S6I`$=2I$ic?RaeWnf=eN9c3xSl6 zGd$ZIE?el2ewM&w=Sb)LdQ|TgUPBbz<0fW;H>X3*%3q!qK_S3LbkG+CtRsFrR7VaG z6fTdW5D|sdZ9!4`mEqvo1fi!6`8g8aH(KJLg6Mc36?lp?|ZsTI!>m^Ncr`44);C9{W3fmyd z;YEUB(*G}{IS_dy)q&^U9SfLla76hQbuzvHW2EqHy$Ca`4^($Ew2A?S_xtOxrE!YW z$1bj|(9pZIVi}`td}LtX6(jjuE-Us+nKc#N#Bev|9v)4?+dOn%T$CJyI)Ih~l9u#> zh7Xt6z}T;ZH#%>O2%sbjnA##B_clQeR(UjoX<3GFz8$8^6gN1QLtBT0f;p*o6PXh; z(>E$gN_KT->v3T)#Unvvl+P%mkro1bMDVv7#^Qm#ZG8l}nM!;(mN8N2lc6svX0a`F zgnbqnx)830IoaU7TTrDGGHe=W`K}f~d^P~Vk^=A z7X&H`pak~kslhEj+gX~uYM`ClAU`(50hdlJ=+(Q=7~kUW{(f+nn_~m-e`G*^`nP&4 z<+yc0uK9d*xGmjV; zn`c6Tv(8r@yKoq#woQ>h6T@yQ6%uzdT2MyuM2Z8ZJA?E}!{vpj?7#!o^8nrpv-w3% z_VgkS$9km7U3O@q@a%M=y}u5+A{<`v-7hIL()bi8n&Fbn&08t4n?wDxWEAlY4($t6 zcjM6GRCo0}unhG`q8HM?Evyl#*EwjvQa(bO{?|zk-kEK-ISQe#r%YedVNvj!iNLfj zkT+?* z2D`U*{<2Fxp>|V9lugs|Q^;>#G`2v7EZ+-=p8|nC1;{4J@Vf z1(IC=9bas0w}qb!{%L7a{{&yXkYJR93*r>}mIe_^ih-ma!cla_V>oo4Ox`^$@K z7^-v-QV3J>%q?i~nC!@_-JE1NaVP{})m69}leVe=?qL6R&H@&|&n$xi6lL`CUO zi`96ZxlguS%x3`MO8!_nL6w>`TuddxqxgcFfyj(Q1ENdb7MY7C4ei#WRR)j8G{6|n z(f#%3NIRbsC(iFrb%zlYJiSjx^ugzEqa~cjyV_fTWbrR#4AM9R`^tUIHJ^mZ#G45^ z)%Y(adi-$BR`{w%sya9(3#z^gUoEq1HL^f9a!gvkNbmFEYIrLU5j?wk`;Utn|+QRfxmV;mGbbeyG!v=8F z93VB_F-l7bmYqZMTF**a%D2$FI!y*GB#7#;P7 zT+X{q$4`=OhEk)MTwKIo^9~M#rC`kBi<_;cLT<0f^AJ};4Q5Br@mwC-`p&+$MFnzY zH4}={qh>nKYoBEXU!HXN%#P^3mWAr3M6c!Qp?~5mUxgm=v+mMc29XbgE2};}lEiIA z4RluK2`5GP?pSfvh$`Pc7W=}<>Mpgbo$u7D_e{7-++S5TDa)o6L7MpP#%2mS#|+d9 zpWyJg5HmcKr2NE$VuWAV7^9zR)L1G-JM&O3(;5awy_j9F%_SHYeR8fkzj~jP4UpyPZ^e_3pFAP$ZC5nzGI$$t;MeIoa#}e-5bYe^zTDdr3No9nnxG>Q;L_0Z!(R7A%2YQgtfyf@gM~^s zX|-~(KEjNGX&)3O7@s?gMN6{V-6RKO>hfO;eSWQh9)RdDNa3)r@YW_1k!q-LILSlo z>&oTP1hxa*=&U-4D}_5qc{^};b!C(m3V>evpMsB^kH^`Emz%yML0QuFq6|`7K#StL z8=8km#UMZp)fiqpPJ_Z1Z;0H!fiv|kEzIWJ{fhdG0ov&INw$sRPM@0Vw3K0!%V&Y_ z$Vd8rawyqTGi~D=R#-rP2&Zk_my=-RG{sBO1|QK^o#QMfW}?jcm3I4Tdw-ib_6E;j zHD3^s@#|)HR?P~qTH$6Ka**pTsedsaRiMA{a&X)<4@k{jTEpl`%sF)Kq!O;F>eJKM zHBSm9Uh(d}JqCSq-G6@6;7*VXo6ljJX-M=a2HW<{DyUaEUcA=z86SEBcF?w!Zgn-b z3Cub*mUVsM-(oL66laQ}=*yJ{-^V~5C5udM(AIT$7}`8 z97!@OF5UTix?#JT1A?2B>NAQ*M17VYRn)+0L~gVH`4>v^ZyET(21ZT(4vkZ+tioTO z8yb4ip{nMn^0ps{vSo0W;p1DPmv8ud3%~*E;`-dHC28(BHjJ9W0sX^+52xl7uYwb5 zn-&IQyca6wX4krRqnYt9pY>H8 zjiXgo;%9@M?L||FaGfCoAIw{uk5bMbdXxWqq3^H?z_h>VJ&~5gYRFLm&95( zzkZ7GyXTPth7t_b3AvxT1k~LFpJc<$F=lw6Qqyn=06Q zmnAK2JbuLdZ$~gA5E@^Ud^dFw4tT9dD|}F^aCyM`!}`TitC|Cya2Ee9v$Ng*5(GGJ z|M59b=W{nXokf1hf{qj!-aA`3q0i1&@tI41=BLq`Wzy|WQTwPGvb;TULn=rM|6bAp z75BDes;f5wTbC^})Hl)ewloA&946eHhtucxfElP zLHv&UchL&+Aen7Bt5-a)xz!apvVsafY8~~ZJ2Um|XIzLheyX%f35c|?p1#aHuR1VG zB{Z*gJ^_qn!;72E{Xzb=zXF1~o5by%CI=nXw7)YKc&i2rJVak)>$jBp8r4p_Ihd-` zD5^Sh=WN>l+d2BwukXX3A?>Nc<|hBZQYc&C)q~~AXl6}{J&RoRVwY3;My`I(K)=nD z-QOuI-#~}=+L5-jas0xBm{*BpmBFktxcqjRg?sA92?f9bwZRQF9I1E;Eq58?8>gJ2 z^cn8k-5Yt=CESk$kE!20Vv`j1o2TIx%EyAGilu!1E-VM@f6iJ@zYP6*N%QSz#i@57 z)s4Y1>}|ez4|f4*QfL|8LE(vL`%{F^{a@FMTihQ^Zwvtw?uKZ7jLm1$4klYXpAaFO z@TrpVCpX_suZo&e#CzW_OuWqteFjx0y*LwZ#hYLL5Q3eBs@UC!>k|NA5$-V0ej9N2 zUH`-Q!-s}=$LchxPF06)^4|2V7*8Fv+4U`caUuVuG7009{OevLh4&R zZpq&Pyk_^mEQkG;L-^?_7)Uh(foM)j3;p196>M_c!fiJEAGI8v$U%ey`Bq+=g$gEA z)kdo%MEJnH12;_!Ehj+&EE?;hg z>~gw&kF0I7;kEGFz>Igx&$!BOT-Xh^;c`D_>K}6+qHfR@>@MXkDu!$6+Hy+H+|$MeMFXR)0`6HHvwG zwEQ9~9XJ|+v4?Vdu#<{7s-#gt%~Z*3JqoHKNTer5(O>HbDW=sR@`V11%PRrb5{lPk zl`+Dujt`;g&7g=6To>WJ(Y=QFt;s=;JYB0Dl`oy*7Ajm>Zw#qTkQ{)zz!r#Ga6TE2 z$!m_-q6vWhJ-$6nn5Fsb^Yc)7!SVHkZ=*p#a<(^9gnG7DBO7EH>~sHx|7@!I)*{gG zep+h#^_=29F!!KO3KI(wSHx{(en4`ct(Y(>`!GGW}P`;?}>+6xH`H zuICA+fHn3Jn>^yY-&6AHLoe_em$UzMSp4)^w(j{p&rWA?p2uCI!#_aPdJfLA)n7X* zE5w4hvadZFS)%m^bDRDxbBR`C(Z}HQ+-_o2V+G--iC0J38T-#1&gj_Va3i)t$#GBZ z$LQ4&O?~zO_~Y-1^KYA$fG$==ywAbs=&}9gFSz=BgT%Rk!XoGH<&XQW{*0QXhEM&e zZcqa^Tb6XtfIti1aEg$kwXrx%ZjIA(V~Zh3Tic(JY{!Y;VYyj8bU0ynFX5*>_QG2D zM7S(lY&EB2^BOH}a>Z$?<#i>r;Pdn5kLJM6E5^8_p55fs-m~Fb(LY!SjQBxxFf+VD z<)3MV_c2qaZ=+TI*|EtVL5mx|>arTtY*>iDGqM=p{(2>;h23ULJn)9BKA9k(fue|K~0QVmGQ@dNi-y+k=OxkSE@zThbSVHI{)qgg)kE3@F^3X?M#c6ty9-uft7J9zGOtFV-D0JzWp#-Fvx zT@>t><1ekTJZ;fD-|yJGOHJq(DYdjsQ9V77+_7CB`aOAgM>2QJ^E*CKN-r0*fULVt z0WdG@e_m0Xk50AQgfbjCS?9=pXA{~6P8&OXvrg z06JBHBPOpnwHz|Y^vD#zTn4m)s4PTU=aP2j9xvjZ?im4yz%Z?|0=J2v+aL1s?T2V0 zU#bo{Eau)e|8gNN>p7R{IXLqq!n-_T zcQG>>>by?h${l88o*1Fe@L=a<)F{I_LwWcOLN0B$6Y`4#=9Og)s56ME-~R4!J4kms zc3UVID^vG}El0Y;<|4*ihVEHEJ`IOlT4C(PnQ1=0UFgdco^#r?A)EvCG!2cA%@7 zid!w{7XZqW$j++?%(H{e^4Sz>Kg?u6CMH2n_8@M(`2Ctth9U@<%g#WcqVN#(&ueg0 zT6ipHAwb=6qZuze8?-+3Q`!e!ahs-`tioMt!MK0gx*7L z*V2nv>@8(_|65hOtX2ew5I_Hi=nl(A=70vJ^jp0=b&hsL0Bg5gF2eT-9p>e+!KzTck@vqwNPnu%YBANE zggidooL4fee$q?-?62)e=H7}lD^*CRYkpqOB( z=ckl96-qgC)X+5`vu``)dAwQI49+D$OS`@HqVP}DQyY%+I7@dEI+XjZ?LJ$Qk{b+F z^}h{O--=6Ic)30i;(nz?iK1(oA=+x6sDwE9dyuB>uqS=^1{>=)knz4`?$~abw^jW2 zbH=ly-Z0o|+Th8!arqnOuNQqklN|Dun0?^W?oqC1-zu|?ILm_nb%dW+Pd0y(4r12i zXtwS1C*o{SQ1T;ks1f`)rJcIP2=~}|v#^(8J4ae9`pcnpbySqod}P`2qa|0j3w7F- z*k4ujfCJ@Gx)yq{(o$*ZiRg8HtGU`Q2v7RHBYX69&fQp}r%t@M*Z-UE?ogXvYS(X+ z{r8~)&f@hnKrfPucQxf(ywIU99cZxv8_03m)OJNuSi z?Ok?uh9A9zenGAmT(+`_ec)462MB2Pi<`>1+$q1R>HwEvT8l@MYgZ}%x6;XKEYV8F zw^0R(MKmSs94pL~1qrS<$)wP6IOkPE7YFeYyZ(;q=ROd+;j{dKq0V0p-{eCM4zc)Aflkn4$2($|rK-3$BEKFyI5YVf1hu>uaQ+D3~GcWF-=-Nz& z3Hl_=wmxC?SF0eNPmK^x1=~5%;w~C|t!*k-m?F-92B6>-RY;t^b!mr9vse&W5_us~ zhoyn>ap+!5$>;9p3D6f1Zls}RHKl{9v z9)E$qrk%R`<2?Wu_5+BNH|E0Ya$%VikQ#!{Hsw!@p*_iD97!zezHg(r28*Kfvi@m7&RnNOn1DGv4*DDHaD? zFDtgMpGTKG1Xf}tv^F)~Xc~gA$Rm1!Bam@e-_+?Y`ee_DrZ85VcjC8OOB#rg{+Y4Q zLMYb8uLP2ROGR-J!&Wxj>eI?U%~OtVH}f$BX`m0aJc%U+{hB5-jy6bKXEFB@3z4q^ zRw~BR6z6x{hZ<&X*jcs96`X>cda`GB2}k!aAfUj{R%Lm2iVRu?5H6dwOR!&Vg~)2i zVBW;)176(bTh+z2D_geR*_PxPh2vs=k`J-R&Wn6V%A4(cesV@XRi;q4%%$(Tp4S7NF#Etlq6%*)2 zEI8>Fi!un^)&_YS`&)wjN1dzlmh61|zk}NqRB7%!Y61j-0Q|##fn%#Hw0R|{T<`>M z*HlFZHbE<_hjf-#Io>~1U+i*I+^>Gv;<%v2x0O?;kM*S}RHbJUotLObV4?nXQ`(QK z?`fuda8a{7=4JU6$76D3NMu}ZP-s5#KN=V1%+n>MI?_%Rc>nR2Pz25g%0Ke0i5_Fs zgj_mG=fP2iHYa4n_Dw}kj)`KZId}7o=8B?~q;=)6s)CSBcc^hMb!9gt<$dU1CoT8u zI7HI}lB+o=zXf@D{hF?9QPHE3mT{w|0LyRgdyldZpr1*0?>W$}_35TcJsEv*|Bpk^ zpEYxW7@+ah7JdBqli3u-D#+2(^i|9snF& z{jFMQck6RRL|@0|d+YqxdN0UD@3PXZhiLl)Uikw4ctDg2z5u_Rj%=b$wX*9zQ&|gs zDZwG@i9%#bm&-}&zO)?N5;<2eBPcoy9=<$~j% zDcxL0DI0VCe7uB}Q!SeEr@<3LN3Eo_>ZYz+oTV3r=84)MtRl@ibT~2wCAp7(Ud=BC zy@8|uS5(nLnA{wjoM%~+ zfBWWW_Q%wJNt6V&B*@l#$eIa(JDbfaYCx)Yna*bW1w5>N&oa2XkKdXt68*J0nDsN` z;yRSBe4X2TL_PCf^5=`AtaBDjAI?Hf7eAyHG~JBgRmo)71X!i+)nfPovUpL2rZaZ$ zF-`=)Ih3?Zjps*Qm#dc9prgnlSEXO`p&{7`$d+!M&4$}<1$(rYo$n94zg5Jk^qQB+ zqy`ihzOpylWohI$u~XlN=}RP%oLFm0U&vYwzy9>>e1V`nh>%s1apGT&M5-n#nDc@g z>g+7qbgAawX!Q6^co*@Ge-i)>tygE|yrn6Kjg_PF+u%Oltv+7_gw5_FQ@y763^NPL zUgZ08*O$%g5Az{30bCNoCTRP6iyHF6q$KubvN*&vwB;l@r;W}pNAl5OqaEo!Uh0hI z3~JCd$NRIw_N`kI1GtkV7SBx~lLdLqp9+NfB!1KsQ5g|loyeY)#-}2(N#7Ymc_J3M z(v)cnf3h-grv{J|86TwRf+|n4pKe_e%g{feK+((Fe*C~1>{2mO)ccgm;dYVslV5vV z{x3pQ7Q0G+S#8GYe9yg~T;mo8H0vbOY*AE}6!Bn#r7N>1+J=LU?k130*QUhO-=`)L ztH>#2aW0u%u3>k<{_cH^J1;V}tJpgk!2jp0Svz^EZqfnkD^w<+2qT&1*94uuLd8D; zb$6bgv4>BXDx+*&ukz?^@a~h)kE07C!HRe`CqU+l0tqd?OU9n>J*vB3=lW7h`yg*D z^$eW9wK6$izq4@emlW^PdF7+O|L@<27+s2iqwcEZUQT)1;&+MOMstlc?*(zC{cX1n z_m)jv3M4^L*Fvv__l3(!nHsU!EGWfEOg)Bf@y*-l%xlKkm)uO{W1UNN((-$AN*%wV zMqSnt#@!t2Y0hq}opatbINx&MKRhn#CNkzyn{giCH6e;QMY(#8eAUuISElm(4(3~IzO$=mCs%JHedRF2-ZziRxS>;-tp^V{f3E(au%Axf&zfB|G;zM% zM`e)}h^VuQ8o_js7(3^QC$c}1;W)xdvO_1qlx$LQkz1o*#0Xd|@j-(Z=t#XG97 zaUy*+y|Fcb$*Iu&v!K<`>e$gtG3k5+zo<8LZnj_FS}eHSQ`rsiwvcqN#w!f@G4d54 zWovUF6S8pTzbY$wXd!ZTA^*k0!oJ$zRpa||@^^!wql{LM(G&(wjoWRqzP*YRXMc_Y zB$;*KkHPHby=J-{ydyG<@>>M|xQIU=5<_l{jKmn0`x}^FMq?bF{t`2LSbtG4O|hUD z_|?3&e?6fOP;$q0f6IwK$xkfO7PtJkA3^85a2^1!3G;)#LY%8iS9|?w*@HoG#4D90C-t80>KE3w)cR8joJg)F?-J9q> z{VnTmxo?^k9k%y+Ee{7LfbG3g0qpJy7aQ;zX-ZVob{F;?AcS1*d`_?Z*Xw<#I_ zH=AdOZXEJqv=h@z#b*p1jJ;-8^G$k6oA65h8%6#Oj!Gk#Arl=LA zP4HfOFe*mbA!FSd*6sg5x{D;=p;d(mM*3@nr0W}Ez#$Uky}u4%!b)oZKQ4>%gSzK5 z6oe0`u_kO-`!UF#0A!jLJz1daeU(ii&Wsw7#XZpucOnZ=%nxa|2`4b@r+DkRmTy<} z^svaz^dvkx{bFY+Rz1t_K;-h|Td}v>i?9goRLz-i61LkvdA*iie^+9UL^J@@8(?+c z>oSBEQ6XQLiY;mv7@0IO#>6Y@eZYp*BRBUcJ!w9CfM~h$)WlDmI$v*r;ltTk3<4el z=bx9%yuP%@YIQ0K$jWnmDTNlX>OH}J#^G2v)ZJXr6aaON{KdI9rLh|ZRb9SDc$10n zP9oyLY7yNGr?QJ{743ad)LJw*B`WYlDApTB82)Zh;ku#HWGq3F!wq0>p(}**Y!h;{ z_@5|Hi4mx(i*xYXLzV`I!bJGx#IzqP{N&gPA_c&JTT`n~k6yvN%#Gk=+<5&vx#)s* z$!%I`R)U5{r2X=PI3c_!q;C`-xU>p5L-p{5GYzH%&Lj`b;O`Zv8N~fIjkxnvAxo6# z+!*?20V;IF@Fbmk7T9P+h%s8njbVoi9cgznaUkX$n>~1PzW~tP*Oju6A+ptqh%t{j>I^}!F(e$;{;!HuMvH6z~>ZxOs~<~b();nXBwT_gQ41Y zTK{I8&erq)>uCi+Ww}d)PAP)LkCjE=)*c2j(pHS_>h)zQcGpzZVRvZ)=; zinPlCYsq?!mILNj!jJX3&9LXbk!wWZk09ZrUz6SSr>|9Q%_q!+DwBx0AIKBIbTXRB zZy-|J5n-_!?Wsv`;U)X;+0&#?&-XKhtnl(TaiILpqPWX>$aQMkM!6@)!TMbHEWe_S z@`=@KF{!4&SZq$l_uY z&FrLJBxEyrYt%x?I3)Y~a?ufMl1sm(Gw~W8lJ@fsBE5YDV7tgiQG7;g+8%KiPs3k9 z@nbB`hPutD(3}Wt|J)Y__QVjCJ4wagNDLGxzrn17*5yxRlQDcwlV* z8)qrB6fsf?4^?A7fCq%h>0anp@5o#sEHg9j+iRl63xxAq0IYq*G8eFrN^)#o`Wbwx z6gBD-Z+r^W3|%irmWAikh`y&3IZXAu#@(`DvdYqaV_3HDr^M6$Rl?%sZ*Z6EDke1J ze%9v~?MYAfaL$*+vb})@Q)aC9!wgFZ(b-!t6A)3Oc0Ns(v zWM}XIS$*)%`8n1BbsPn|gL=1cnPJN{9+VSlj9oFd&isuskINC-G>Yew?pGObXa(w@ zn_*NM3tVX_O#;`5RK&w|oX5bNZs!Tpc_o2hm@B7`c#TPAwQWVH9D5)w6MzxKty6{5 zgO4f!%Y&?tLw>J9WZXnZ=tE4>70f}cpvjXY;P(wo2MRNvf`7N#&iN<5`eFr?2H01l zdQR`HJ^0lP++FGByY06?NYfx+k?64qpf=jS8;z7D*1B#u7*k;;Ga{Fw;j734^{6g? zhu}*NIvK7A%4?~!OU|}D46fNYmlL?D=f_3ZL-*zug+Mr3Uo)pW0x1*UIqYG)z6n9Y zl@Ve%4JGK*Dw)9#4KJxXu0mj!#Bj%pefIgN2LJUH=mA9|zfck9K0-FZnW0-1d&S{Tlk_=Y~X}>5SIT&Jrgj!#G8l+eAdZ#Fx61yG#up58D5Y zeT|663`4sT9FOqh7n10AByW@`YO|Dz$uPMrQDqxda}!S>FJBU(Qlp>|Lb9p$Pz50t z2FcWfvGy2Nx3FE+LdNdfOx;K~gfX#ratJ@W*y}H|VRGHUB9tu3y70S@a04a^>&eFr z2BAcR7g^Wxp^@rHbuFZ)IJ6IN13(0-2d01;E+IoE@#B+g1H>bN0h{+8lpI87=>T$s zPQZ;$Al70mR(Zripc`aN2+X_Tu#~a%UVm{qS+GqE zw^rMm)90b=prsrY`=JWK^mE8{m-;gKwpa;2g{bxoeh^%TFM} z4J}5iPWgeuQBeE^s$ET(V0EJ3HW+eT;`Revxq4FZ7lu!~BSu{JGeu@kTdc zXaNL&EI}WirPLf1dA#a%#@o)pGnj~9^_l`v2}v6k>sEz4=;s>DqrYLU!}19E@H4XW z5auclMw9_~4ay7Q&qF$kR7m5K1e9Vl5uN;as7E4LD`VIP0Z1dQM$Y#)rB`Z{IS4xr zs^BGs+jT|5(}*WjdK{!utFcjQp}xRV7OLJ+Fzn-TeCyS;a^qZ-p!c4F@76c})#_VIs2qQUZuin=BF`9`$UpL^=pw(hunbZ&Y!`AM{-D^ss@Bfww&^ zIlsbLt_QewN8OD#edmgjttQurY-3YGMn;h^aMOxE_Yvet66^8Egwv$iSAGwmB*;Tc z6w7SBr~z08EiBYY1D^029q$1V>|sd&fyz*61S3RUk;+b&$wEvG$` zAdQ;^b5ezge7jrpe3H=BX0t1LoNVdA;7qbeVBB%ni~B_-$kx4cfm3fF>ZwXo3Uvmw z0TF%MyRD5onn5DfSEghu9ti8Uqc^H3RzcS(yownnd579`(dN)w4Ua`Ez;z}HrY3q% zy6NyvB>^H&Is}c^CWHl}hZH$LKL#U2)2ZKGx{HMAa>}fGg-Lyn1qTxDr%l%-aG=9r>1`#=M z5$Ev>LKZb));(~Q8`?-}W#o19*dDKF^D!`BF5OUtD0skMM|9pCL|uX`B;ujT#|W1Q zm4phzVW)?;mFLtuq`a}ug}2ERNZ$FJerznf#{#C`E63vgQeSGHuUq#|1p7ek`{+~+ z)_+XfudR%|#XoOAEWpL~WEGPXHtLRV^8+tB@0I?OoWBvsG;l`w$>2*{!O58wq|a5L z@KipQg@G3+1{KyHh6H>~D>N`7!FUe)yD*Y&9EOM=Pf6_n9dI?A3kKa4yGSP8TxN+> z$di7wK|YzZ4pRLTt!@KlFc&4lm9O)-3g|=QRNw%hf`wXfzanVRIn9^=Alom7R>W0C zMf2#Y3fG|&^ei{EEI^jhQjrtt=nYj^jy7j?8m!~WkQ}2EYmw@4lZrO47~W|jdcX&FGl90rBDnnS zV3{=X@2WhJO7!1YSWc!jUDc zas_(#*21UzYG8_s>*o4{uMpmkewI%b8TXz$5Le+2jhyzT9OWE?y}WU)pFs(`YYmYw zava!2vVV&#e`v|240cv)nQoKiw`~Xy)o}5Wo9{=3ez_srD7s*>5;F-@FIfXoC}K@R zOD={>)MF1!o5aGK**>7Q2!E~7?!ztqs!z#6CmJ!A3=3iGsv+d~D-&5qcypjbMK891 zju6<&?%{`g$~|6Krqrvv0hK3(W_l!2zmsYFkDN_Xg&=>07w^DSR5LJ{)mkfEM=9|D zoKu(mUK2^}$yWo-*Ysf@^z5=y-Iqbh5c^GOzPxl=1(pHcAVGA>BAbT^)>xV@8L@-4 zH=};gp-%1_NWD}p!e}O1SqgzSY`H&_uF)aCff*hs!!~*s_WxQs%cv;3uMf`*Gjw-L z3JAhTNW;(}X%I>bAt4~$0yCs24N{Vl3Q{5S|a#+NJc-hN)3|u ztsSl=f(l6AiOwHWDmE2aEry4B>C2yHAgz<&4tm6>0y9)=4ZO<$A|sH1KPlkB2dY|W zf)h>p2@+G7Mjw>iyL;ALiYX~B{h^(?G`R!CC1+r zk@ISAC^hxm;Y6ezrYL%5(%oUaH}-vwxg34Ujk<7ZPiofoTWR+ACnzBfM$SC03<&Yk zP@({bojK|1G84g!1p}-}UVd(lI8VzKUtA3#KB=0=ZL92CDgy295|pCnHz2R6@mCYU zf~{U>{*c&%)nJCM0zfcyx~w!<8Do?KHW>)HWBa5jvCWtzJSsudO7$=ElNT7snzc1w zk0HVH8jB%ae6Ct9U)aVfhN2*HKP-SBS07qr%8EVK#?rRfTKgApFAu#d9Tk1m&SXblnsDUX5_ zM>hoTwb)+vBk(b`h*n)<&kpBcapX|$wi;zD6j!4M-;O)LkTQEr^4$YrQ~45<(~hZA z2iz}FMdLTb^S_||AYAG>OzMCMk4zt`D^BHb#57C<2K1eJN4mkr^yv=AWbosbLv43f zgNd-r?HHI8N_Bx$97!7Cm3-MjxdH90k57go+4(t?RZ+X0Bs>aS-=1f2@5eL5a$%nr zC1?rxGPH7W$ONPhM4OJmIDfos_>A&10+afY7@x;P{P_^oyO6UC=}+`mH}H7RkR%%~ zg|LfD5CSPWr6$l9aDk_n^1c74+kd;=*pd~*ya9akgIyd*+4MOz=m|9uzUC#-y?$Sp*r~PH#tbiB2i3;rQw881^Wnm8V=HGOn5k^ z!j}AmdSuuUw-o?I`#vUQz0M%jl?~0zfHB*04fTl?IRQfZ`!njAIDcN@|JGi{VZSTP zU?k`~LWz@HwiP!-Lrt!abT-{!B<;~5jQC&;4 zRqaUp+)JXCh-$ctBJE6|hjz&kP?2+r2c+YomBy5=n~3}>h9`M~;CDjLXgd`YlG5I( zt${g6`MoBh-V9h3%|(C79m@S`ajP&LK;bKfcZ^zIhi@#=J*f zw?+H@hC_tYkG8`OvE$d-`+NJ`iWhVKm7CU&M9NI{Va{7cT|(wEo$JWYsHlNgK-3qC za^EGqL!I)DFjSOa`5T^gNmOaGK^nrKi@TZ|%gULRh(|zojWx>}qx6*t5y;liD}#i7 z7A5X0&Kyn5asqF|)Nl;Wi+HD$P^JdXv(p6GzB8dcT9EfZG zBRAv+%d0MeWx5x`lr5WM#aj2-o+%+C>C-tOkg_gNSw^*mEE9>y%G81RK*6qUT4Up@6lNjK!6& zG9BF^JbYXj%>%VahHEAh5E`dqh$1m^#3*U8-pOrg0UVqaNgy@x*5G|fvriZmdhh$Y zv*zN0K-DSY$%X#!`xgGeg+CQ5n~{G|-yc>Y1Z`i6ls(j_JYD0fOy{F6Gx~Pis4F@o zCSf5-jwR2Hxu=~s)DZrW=!p@$ZbnJq03%8)^?;GM7D61r<8wcuAOcJ-=-{5d5xvl}0C%O=_ z!3)37CyT`96wc!B6)77jwqxMk^YyXOf?7KQ*K9NVKgM}Or z0|llCDuaQYPz@d{FDifa6;=_L8qDwpYNcZP4=D2q=hx=KLm=YqDq!WZ@en0s@}Sb> zP#8vSu$d9Hu`0JP~<-MF0vW)rJj=98?3H) zJOys35}Wv4rwA;#jr!ETCF4NV$xip7_o`1a?s*rMeGe)x4L^IP?JR+PxJZ@C$Nz|G z;vE5E7|~xEfA3|!P83QvzB7)@W1tF;h#Z+X=SGJ|8u+!r@2fNtnsGf2$+G+vOVsBH zoz*2Xx=w1^=85qrgrulL{w7Q0*=uV%_dyw8a6JOeMmkT zX+9%}u=rGk@>j}3`7aA%y{=GyuOYN9v%&crz4N48$S$xpU3sh=sI=6tvx;1FK@GJz zX^izhfbp*fBK^hiH#Hz~O4zk6?%tsY$(&mNhkh6}$*K-9D$NX4Qv-(^KqmPiKL=QQ zgQK?q;7{Ql;z%zQm*|O6$szt8bCd=@x^sW*DgS%3bu;qJq1e4-eT$?fhxT6@Ek3Lq zQ}X(-s}tnuFPElNzid9j#;$!y}qjC+R%PXI!HC{%N7-k=n5m`f9Pk$ zorJ7-PKvkNL!y`nu0T=0I*?$$ktU#lwYaTXh4SL-5fGMHr0C)+*MQpF9;#`Sih&U0 zm=GMVGe!^pzZv}pBFlegZcZA9)mKweo4zhaq*L?Iw2i5aNQYIh^y#mQ+LPNNO8N(x z9vH_n(B)W@@ck|FDj8SVA~m_LD1fzix7+AN==Ik&s|KdltCn(u---&pOws>_Amu)V zVPd`o3E-$LkWzbGRD}_f-%gwx0?!xwmGP%Jc$Umn>5zE@iP9Wym|4g(hQWP-(ow)D zjMI-7oy^M9jERw$h<;6}UVxDjlmh2Wo4&!f7}Xqlxu4&;0Wa0EUn0*xV|X|WTc1f( z_0ofa)tW>+<^Z!YE*)wCG3R#4W4UhCSU0}hQZonXhGRXJ=*r+DhL*5?N0`AIkVDQJ z>$TD#V>D3iP$IzRjzaH*Dh(ncmD@p^qnsVde=fx>OMXU7h)-oux?`=*!&Mf(h6s&i z2;{<^&~kjB@2Mi>=#x%A`Q5Edk#Xae+e41x0M6S?&wF2NkA40IH%TH{eUOSdBSJAE z@-i~Qj_!ChflF!Z&8qkscw8q&|@`_Gp8i5bdgPijUiEe|s#=gS&ajqV_vq8JIvzgjGJ%Pn^w+7lxLMl^?)y7nwqL*d{*AL6^M)-jRPf$cwnZgRC)AKpIS6^ap9eCa%!kJ;8ZqJ=af;ffc z?7#jmAVxuxA=THPkRQa(e10{a;OHPsHjYB+w@15@?^?Pl&dhyMDuEhmks!H>n?1I# z#OJlW*8^@W8(4hOcfC#Y!x!JJL%4+dk<%u8C$WiAE|1y);)oafe$*@Zn7UZ@B{};7 z9UMwa3D-_A&P!UWrfCaj6G8J65k;{uMhf{1gw78jG*n39(}`K{$`7Deqr{3A-6}Df zl(7s*E-C@FLWYbdziMd+;3bfZnh~kfpVwusWn4C!tXgsLs_PP5p_4MHpZ5}zjzB3t zjdtM&Uo!xbgdnD!I1|>FTKKe|@eXd>0f-6|ojFQOj9@cumHLWlEQ_=E!Az&{x)HL? z?UcO9Yj9p}9l{DekxaQ?Q6XPD=;HFuaqEW3jpLE^%9!`1`tn3(C4AWZx>k|g^B>KQ z79!dNs}-NXZH04?7A=}f2;9eURe2AAF2Y>Lh&Q+TY>VAuuSLWwsqak0^&I@WyC7OT ztQ^Pnbu|=EACAOy<8|hXS%l(g5FzmjYPx;rlKNf1tW{I+{_g`%Kzp*y*?FV>oQRjv zh*ose;rP2;z z=~j?5$JY^6dH3fT;fnD{QI@qJcbZ{Kh)a2qwi0Os{0x#p`kU7WwS&Q>mea{ZFPyV=WfBr^K zStbNsqru!DMj^!rzNbk8p$)Fr2b2AZ*Nx-m5bW;V?^XWWe??3%T9Jb5``Tqc0$lVY zhV0qfG`32YEeCfeYk61k=6QC?Gia7PUxLn1kN{iWsL#LXAyt#Qyeu{JRX%*>&{RJJ zT%HzDqdVRX8?++c_45%Ut$InNQ>AQ3$rY(^NO}s=7hg*S*rY5lM2xR*&f#3Y2JK$# zxBVvCfr@aM*uL9=Mg6x|%@%Uw!Zi(LjFm=+u{x=!eUNTSI@1f8ckc^%@l^^rzhtOc zW{8?0lV;OO$me_{21eddKJh5xD$}2yIt@Y(G1wS{A4m?BUizwIWHm~Q+h=txG4tL5 z{BBaLa~L~9@qFjL5x`qF6F^S?SpRBK>5EauqTcQj8)}bKTk@w9p{R_2aP-gZ)B~xF zRR~@bhiMRztX@0i$^vsGkNJ5-yCk(eYcHn-^cDv*A>FO#wp`RNbwn+|3#j8RM#o5g zCYXq@;xmfJ=vfIM<4f;B%KGt#`tCH7^v%3^)wEK!2=Cxg z?D%JuJXW4XT4sI~cB@^${Z=xd%r_FvhlDKAnO-Zot3t(4NQ>@>WA#vVS7&Ao=fboL zu`@DfSC2}$y#e`r%}0fJZPI*t7y&5CpsLS#zMEWWk~}L1B4(A^AAy$)8SR~|JrvXq zOn~9Z6twFhBPN*fY-bZj6XE{M9zJg%EFArhxDXRV2<8FI@2(Em#i|WZgzO0RyynMO zQE7aHQTaMHJ1F2&-)lu~RDvl^;%$TmlF8EK2^RHuJ*XrJl%IyaP=OoRHdCkQ$ozTj z35wVp3>yQwj}jDItX#>@%elm27+krq>9ibTuV5-^6zWpZM1dBB2S_M&81KT$-RI?> zQH(}i0IqRMR@b@j7|Q16{yhyk{Ty-03mxL3FnA{fou>J(p)W?zGCmY>^$~ExP;5h* zgzn6_JNS0niHH`wylZ{+1hl`0R7)g7O)g!nJcS#AqFJbhXAk0O^tW+QI)`^Sw? z3A4wUT>?YEGmY9c+xyO!%O>*2c(--LJ0xiCb}-e$zh3JJH=7iGi;n-YD9!!*n(0GM zaa8a#$64K}v^{|#3Eieuf?(pCwRUHB?yW_gZZ`{&JY7s|QEb==84@hDoVITH-$2&i zZwwcb!4Z$1vNd`ir`;b7g8p^xZfg6x`JkEUCd1+7><5NshaL)My%iz9;bwCA#*+fm z5$siTKTzE7d1N&h@jjOK<8wvH{yvg*eJXR8r&s!T?H~9t=_fI?-2Adg5;w=gqo`iR zJ{C*wL;b5jztVB6^taK>dWIHZjrG){k*h!g5O&Am9~SfqYWaehrG#hfa%NqyX6}%~ zD}}ewUG=5|Tttf?y{cWiB__9ynmG+xCiqV4MeLk>pj%f!7r**_=tmtFD~st>ms z11E-Uytgu^bSCYf6>mi{k&dK_0f(4e2HK~$SSSU@g<6herlN&gPu|qeWOSd-FwDJj zQfNAAv;+bkmJdX^7j=(^=GLQT#H-3{&^U>AB}2ah{uY%KdfflMb?w$43s2goO_!;m zmyZSu4v1F_1GaXWT%8nW_%BP|)jhY&NS*hc&9Q8FbvqTt`pFbKqoYTAm8E?40)+TE zL(b2pe*Bm5&+%ms+KF4#tquM@`FPyo_%-_d#7xwW*ZLZ-gZApX_Y4CrsS37euI=~) zUpBP8zIY-JDf{S-%U`3dyIhJBYDue-=hwAvNkdJ)LhJ6V0iCf-QET}7#+Z^;`1q!m zreoVY28I`_;&a4O>RJ!HavJ}nbiy@xjF0Z*ol#Sk+HSpo0)GhhlKM94&6S0T`tH+F zXh+r6BrZf0bT$KELn=_s$0ew3i7aU_jLTqxu6#_yYDnNf{Q17b(&o*xH#<0D>bZrQ zR|kTZZdKkT<9~@)8qz)lY!=-DIY+-uS=<3Z4&36CN5!)(5Et{$zM-3k$EOSX0I>wJ zY$meBWspB+yr30cAiJ9TPU&EqXd!oyACApLWM_DPl%NR|U(U1h=vk zxG`nN)oQxB_L1P&5o)wG2x|_w9GSE71uiJltmLb?T@zmVfE1buuz|wMy7s;Xeh{cA zqD2QhM>K4jxQs<@-W$KKW0-U^Z7fqU@rQYDtUnn^)z1MTAi#_=yAy8w4Mg>i7hcXq zhn($ihvjxW{$oQUA>zSy@y70@HTz!FSaKFpq<@V04 z4wl@D6-pwcKQmx@l=jPL#&E|S9-p(y-Peov$#QPoiBcr|5&6>pzM{GA5$;}CGx3qzAN{VwX=C;d69O1zFO(l1o{=?7eZ zN%&HAAI#?6r)AOrSBm?V3*{LngVC47!?9gJO*bA)SK1mLeHY5*)s5*9q&j*X3GwX- zCfb>@<-ivNtZyPpXj%@s4c>P@6|+i&BSUb!$_N_~Tzd#g*G?&%up5Rgjn~b9El-JO z$j_F6X}k5!+-uzT*grz6dzoYwz(eBn>io%8xz_qVy}#}+>VqgZ>JtHXks`j5neSfW zv+EDnAVDUJ-hPpYbZSQF&6Vt2L+_K557JqmpR!)8H>R4~kzHy>0N1!V08&e_CD}!X1y1!;=jFu+LgDzuaA6F$CyU0Nz9(F>WZ-s zWL*lmNW1s*Gb=xoAvaUp8Ba5t^qtyTmcu=3T`<49KDB+pO(qu=&(?swHvJ(qs_2`s z5b?DKZE=i26fG$khc5C)x9!{S)LxNXPK_@QUe<4)zR>7>EJ(jzGRsPuYAwzSE#@0} z6!dTLllTvIX^Y(s!T8Zk!86OrXSLx!hVa#Wqq%zpT^UbUE?1!zDtH&KJuNsi9_AiP zlbt>zt__X|mL)N|$DDqgdiFx-vOlex8So_5#=%|@5h5S(CR378ZmALJgQBN;_DJzb zjLeWP=|9gqfzm}8b;%z^TFz+S`yZV;Dr3H!dN~9bUlaEK(#3syMj$?}AlEPNnT2;74M!MCE9`MQsc!zq4} zsK&EC2fGI@-~K6_RESa6K_>_M*+2*(KqW%1^=-Z8!C zk{qlYF!-18Cc7>tn72(LUexpVw>&cTAZJ*2kO1!D!;D|zE*2PPqhb#&uT2D(V{I3| zR4UB3eGk6iSnEx9=Uz3IuLS=_;KnHv1DH9vI~$5;M@=6b2LVTKuhMq>V7*X)Vx4SE zNcd$1?Obwx`=7zFja$w=ET#Ip_1zb=9(DdCV@l%px()Eb1 z+L89c)cX9e$#(niX0sjGQVvR@W`kt^$=Cr6W@^@<`ANl%bQM?TE|$J~dG}f$j^nN& z@%*l>5GH&#OT6^|8sj}FE`8^F+UnKB{IRIHG{9<=wT4rgUjqXm{ND&tNoao@Wmjjl(j!QXsZQeT_q|5^QW6&0E-jDwwR>E~j*N@YoIET`h(M#{n- z+ill1cn@qKKK1?vVW(-r)LkN^;{Ble`v3Yz^O>fSHp4QT6#Jec)Dy_XJZxK!WX< zn?{nYOTN$3=exoK9NMXuP5xLHFiYo8#eG;IhQ@`r-Pf7=@=E((}Jqj3;X$z&my?%`lx@cUinW&mt; z@{<$TlnjfuR3_JF2dL2S%YIH#;(`XEiNuG{Fh9&7#J%ah!bho@5{2EL&fPX?mB`EVN-aJ3fyl~;XqLfa8AallymcP8LTkUknaIAlbCkaDJ7qFFFx&IGT4; zZm=u74K8-%J%l9r)L5W))5|&>2)~duJ5lVM%O{e@$7HY5q6uNcs)qO9wPs0!dde!( zohyzHM+OXpGwGKLY`)Wbxr*ni8+<6C zNWp&3GU77hZ)BCW>l%^U1NYOf;S{gy4+ZJbb!LRwt&o?4<&gdGDpH{HcACC9-yqb+ z={)Z6TO6hn{9yJOj>kj$_tk4$()nv>g(H8C{P9QGD2nr+-~Mu+$OX`+vF{-oit~Bw zY~(DNf224*2tKGL$|>dN`WCkmG4m&N)5#C-X7!2Cr@Q6VsmqpvWk++^pZj*dBrfW5 zFMs(6QqJ9+8ZZ9EPMg?ulDX5=dnFiWu$xKGmiEJ$uew${f64g0WW0WNpk#}AV}6}E zzBOu#FRv3N1R1#^POBP~ezIqB9 zLQq4&i$bw0{&pRGX1>GknFlkyKg{Pfaz;EEhp}v>^Dvw^Qac`EwfCIB#p$8 z^U?{P{#oRgRKUjNMMCgFWas89^)Qg{4Co80{a7IC0-Z^mLxP-V&qnq41X&t{iNZU0 zIx=yb0sU9{nYcElJ18sy@|t!=MuTw2EU4pyS;fU?I)Hk?9lpJ-$BC0OE{99~*&EKL zLFY)uQK3JWKI;9v;wH9?UaD`u6Gam8LJF$)sRfJh2>JXKbQSk*H=FkE_UIA1b(DQ3 z6GhB;j@yu{rg>a%Ns}rAd#(22X5nRjHI5gI_xSt*kOI)p0)7WNJ$}*jzQVqu%M~4r zIoNf77ULCLa+Mt2F&RI^PHekx5g`Go+;#||D(Yzf2&F23P>Lq6(qKg^+7Yp$2^?67 zKr{D-kZHB=Khe9h?;_B?`q;l8Z@M&&vZ=!^hyk*}3IP_(o6vn@7i)Po8Qw z)6T(y-j@Tz>FfH-efe|a{Rs{|?CpXGHgizL#xMp+Uaoz7tzg(0&y;X*;&o!O_I&ub zRAXOZ`Cgt@WM}?MoE3!wC|vI<2AR*?Wz;yOU9G1d%ri`7W7NZ5C#G=ba^ejk*OfX^ zsA_K$N*$RlAh^#A()3@1n|Cb7=n8L{NcBl^*}RZPG``n)@6KjIjLhA9=Gb=4WmY%a z>4To;SAijgO}=&Y_)qaM){CjjO5~%TW9~id9Y4HSEq?#y6uy=Mdv;(#*7C^1j*32; zl8F$R%RT5WrjwgNj8titdu@nd=0t%Xabc5%igjB+VJ4C4dFgPfvL_U zdH`f1K`N#+2eI;PY>F0oCo8NjvK(MM@63nE)d5QbMSBeQ!gwo^IF8#uLP|Mmj^+>y#>ROL~83KM|2 z9dGr>x3b|0PXDySxO@5a>79-t4EsBK`i?|;G&8Fz10P!H8>OSQ%p}8JE-1e?dQ^XH!ea60WlTE`^=&xz{ z_$4=S?OMb5r-yTXAVYuSt_HE&>`RfelTlxRxa7;Y@ne;RGTUm%EZ~9{1catd7&phd zmeBIy4u1RHo|uw~04dUEY86det%J@x-Nc7Tn;Vr$_ z*X<30JfF7B|4Vo=-`N=_YPTAFCAJ?A#v2M`Dr=2Ndn@P6v%lsKP>`R}*5prrsxjX? z!VD3i@DLpF*)Vg-q#RBhe1n-}V^9doy$N~GLk@O5%UzGZ*WFwejn0fOhiVa%%gsNN zXEGHBH>v>vJ^|lY^6AlJ3!Zn2p#y2 zJ3r(?G-z203;Ic!2?PA=GU?9qVP67@=Mn77^L%H+Za*ZiQs038o=MP_IGJF@Swg0v zA)A@>AH6wV{!p_j(PHU9q!avNH8bHaaqB#|3x0oBl`p}6zgE$qj|&}g;47QU)?3N8 z32R-d^f_?y92()2JxS+NWnwocjiNTDS*^YS2skiX?qE#r$_L=7Npj*|(QZ@2)A_Jr zd@mpO*J|(^s1qXd#T+7G1Eqa}ZR`My;7>;E2FOJ@0qWDNQ$j#K-fC?Goa86PO0AIk zT&eUYi?pzTWL&#gXN1q;ii^_%dr|kv%_b+{6bF%Uf`@pptY*`t!PJv`4*3r%(O>QzQUXR&X|mS%ai^nxX3CgkeZs{>@OXkDw4GwQ#uy+>tw9Wt_;X{bY95N3qTO9z2dC~EBZM3@Lkm`BoQvy!*k zGaLji7^1u|EsGFYaS&vYMX5v=)Oy68UEgW_))#nn^=VWQn+3P{u=U=az3Ddyjqb89 ze#lD`4$_r;>TpIr_Tb&0{R^!*z;*w@M$~AQiJ}+}e}JPyKTJ-9yv{8^q)+&wH!Zi@ zVkOdP5A^HFf7a}KRs?~YPp1^fHMyE&2II0jdkJf=zCWGc>-S)(z&1Jy{7)cW=I}P{ z>UQ8UxE=U4y$wm*5}86C`2Gy^?B5a0WomS=?V ziV}a(PMq8%<@i9n3 z>xV3?5k31>n1Dt-hJvQPOIFTF(ek&>;Ck*5-du8*IoE#y)~uwPH#dun$xieGgk20C zwx|#yyF*vMSj)>zu`93B##zGUjJt!L`Z{7e2dEwB1^# z+Yd5-qE=g-vAs-oU5D9T0S)&4^rl->wtWlRMm%i~#|f)GuzceO-eUNq^O;xJF?G%{ zwVsk@yr<(rn&#Wqa5hu)kh^=VF^DkIgW})NvSZNj7RXS0^#_YlqaEwkC~eKPK-tfp zcgGZV11QpHF}Hn)X{V(EfYH7xj1^rEDnbte2jRpqahF@kxWcy*u?J3FJZ^{WgS7=K z-?IAyW4?t314#=|(EZS5mBFYi@$7({2YC~XSF<=)uVVK_DE<=-?W{@o<1gGoJ_ZB@ zME(3J#_p$r7I;O{#D=r^XC&xSFfSGHd^h z3cAo2O+b!;%tNX>l_KdxdI<;qo!PJxyY17cmf3cc7mu>2%0QReY@N%)yBz1yFB~P< zIB0C1#u40Y__8MD{EL9Bk8&u!9L**w`s)LfjyMgseJfI#D5Xq~E zsN=Dj;i+-3HR=ZueGASXxNcJ>N$aB|1;-7|p*3h29pumn)agotEPHb3hM1*+pCX)l zkpr9-s?m5beT`){HQZ-bj);yoCppN2lv;ThZve4+i> zFHCDLvuCidJrN8&waGjMVpz=}|B^v}Pp-=*k3+fXu6zn9gD5TFt;#zGRnwsq6i5IH zy9D$Nn&mo@2#S!fM>@Lx@?)(b07&vKIU`#HUj=*Q{!We*0%JrjRN2I{CTCR>M^$UO z8N>jysjd`03xR@1^TPZt#mLuN;}J&~&e?ndStOK65SloZ1iV&gZD%k}=P2zYo=*5i z(L`wllVE#?qtiZ?;+R@%oJj;P7AdB)cq|hEX{3ftBbisnm{(8MhSb1}xYm6?@D;0z z6PPp@L0IiZDYdu9aU;9@a63H6&J4*3e;kS_+Nll6N`S+$k&#_mFg{o1Ax;#xh3$4| zu~K@&Y=vbQM4qXk8XhbiEd9oLEMok&=iiBN@^DdU=-h?XEJEL;@N&4r3>2Y+HYKPp zx_M1k=-UlQ(`Ts9(^MSR7o$$2_4raqP@#?OTka8IsA+x?OmaP_ye;iTzV6Ty7KC>#_BQ3TkabYjSY5u$p z9kI{9SrJE9zlFVtgDD2%Y4kRo?^+f?O44xCB5J+}MiK1sOvvR)byN>bC`aM+!Kqj) zoW6myC)EN*BFVuOlXP9|9kMS=>MxRBtV5IS@3o}DxiyNe8f3Y=*lsl`tx$YYZPY=q zXy8l1n=eUfNZ0Gew-@7Wpnr8vcI<^*ac7b`1W(7%T~sPjjA^bZLs-5aXw?dtS4+2t zu!ube4{@g|DSoX($VaQ2rNS?n1dSZb%B^G{eTe9h#SK5qP?*}9&}g{M)Zn1j&@6AZ zBd`EzM@Jvfdf>ey!mIj#KJSJADpxNta^we2*}WIh7Saw95-8tRu2V8EqIfs1Wyw2&~F8s*Qz8e*~n8!L8byl2rAW-_E z%%;snCLAWTGk$xgid1eP!wp87l~Z0O*%aTwYyq2X*q{9VdRoKkG!4KXkIDSa?Dc#@ zUfk0UGW~0|%3FndkVQD&8pf25&J!6Y zKINx{nX{=`@?(=m`H!#9Lkwb13)J~a*_AfS7DGf%UeB*;$7 z2Kq9{&p2ALYs?&bt!u zjP?>7{3AHhio4-n6`A7hvN`EW(0(07X|2`Hh5k2C;Iry zH}d{r9sZ}pe{w>T8t9LviVKV}2zJxZ61i*SSS;Ss^khiTGa?}g?+gBNK}m~YzQc=% z+%Jn~KlQI&%bD7>p^!gq5$R(PY=JB6JFk8V0|+&Q@W0^@BB7D+?j3Jr1DotwH6yK1 zQc`X5L4~Mn%=%4=FEylP^(D6t|D|{SlC$a!$0qIO)|=zgP`xji2cNt(9=5CX;U3)RINX>g)~6*v;A<9^ z#ImUBGJ3U#ZZa%Bj1FVN%N;4kQM*@RQrxYso<`zz6xT#+S z^mr^M=NA?sinRjkWD=QRNS*B*&?n!|_=+(fmR%p9Y2$rkN!~=lgyWYiSU!e%&I6de z52lS@HXl|~=SBu7_<6BJ-m6v!(w>GIxowEKxj%8^4Q!~R>7K{rQ>_s#M(Wc&qo4G~ozd$)+0pC=02-sbG`@oBrr3eyv(x$!#mO-UYmMZ~ht3vN@euClPb z@-%Uw*yJViw!@NL-kt9)p5v~U{`@#&*PPMchiPpm`Eu3Z`^RnPr;C=`!9}MXkJ|2* zTpQjb_m|BK_=leC%;OS#ya9r3|IXQ(4LGdk8iIkQw|vrhJkKdSB= zXaiM7p{E<-R^{Hxhtq!Nu{NxW*&`OXez&TpDem%zQ^bM;?Pp*9)RoMXn0;E(# zMBez3Ad|vO5aFP3@R5-2o3gwa+v`nLPvWYJ?Ook+{CFJhgV;{P?@lbzEq!jQ?Vl*^ z+&u5jCPk;K(jxE9R7i+n&IC=o!bP&!EK|;mSV?1ihm8LMl{eH)K8fh+WBXf`5iTz& z#TAoYEZ&Bk_E<3KY8cSOV+fmQr~Fp^1yZKm{zO3Qk+!SIhwlwLdsPZws$v44*w0pi zCyD*^%&5eAHpnmx0Sh2`6Y^ClLV&%L_d~u28~IrG*9jiHgMrP?-+zG##;U z;NXOEF)IGgA4Gn&5q>9aGhlPUoy~4u<*e+sxiRTV{%+_QzZ3C|oKWiq`g=|#vIF4N z&Os`tfJYH@J&UL#F%-PQ*kyLQeS-t{w@!U4Tato^F2Z`-_$fBW+x(yS1>Cia61m@d{*4bj-T?_(W_g_o+J5Gq}9t3sQ zfY*FC|DyK8ic4#(M+?45m;clEX{F(LNnv`l@=LKos%a3zC?$RhZ2yqfK!mtGsUK-@ zp{EVS+Zv*`AMf$w2s=k(+hy_ArkU_U3Nw{SNHp0RW5RBT(>%!x^{~&UtN7|Q`D#E2 z2b=AGvco}0ajV@km!h`=ml)b|{EY%}GP3+ZS;p;%cju*jZfaDqB`nu(@6KLH5Rs7E zgV0O;*}uKTpr#;=h;4?`GkV4`Co*O;Iqq5W%QCJ?``kji@SBL4+L^EE+TCVNvm~l_ zWb*OUf4`$9Q-uV}=p~~jGEz@bn9(#JkHHCPok8N_eVhEp6C~FP?e%W1#p^cAcG@}Q zpPIu?W4?`n1&m7 zZM!X3&k*u&V5fWSo+V4G$rh+C)UpXx@_$EahP#j2_Ao_e7Zmg0JE&*2F%FXV1jLzB zn6MM4>ZY1L49Lxp@1-;8`1mlAclut_vtjb`CrIUJEWgbv>6nW0-5!JPs#hGjpAL*l zl&#+5k#r3T=)6&zky!bG#-@K2SAQqAA&nu-9-pQrGam z(wm*zpYWI8N&9Buzg<)oIRB3l)VliF7cb=MllcJP?{smm+h+D`W5>`$13{;JuP{+LB|@--=e=;t_kp@aE}?mko1O2r-o`3?!Xqgp zuz5=^&|_418Jq-QS2jX^r6}q^#136K=akbr}A5v;j>4NL2s2k2Y(~u&+zm~)5c5U z?0Ta7>0#-%v@;i)Kn=DVF=1z7H19G(mF@FME1nAFJIP#tx1a&3oGzYX*r}T;DXYll zR$GULRdid+e$%6K*8|Ia-kq|LXI6e*A%3SZycB4=#VYPDj7J$t*;RazvlUD7b_y`zoXj2-(R5JF8y8I-k^u2qMC<$SbDt>LOR?8FpGooKlV==leN2mlJzLxRd3^M>`=0|i4g6gl zT +

      5. Latest updates
      6. +
      7. Citation
      8. +
      9. Hightlights
      10. +
      11. Internals
      12. +
      13. Usage
      14. +
      15. Tips for using featurewiz
      16. +
      17. How to install featurewiz
      18. +
      19. Usage
      20. +
      21. API
      22. +
      23. Additional Tips
      24. +
      25. Maintainers
      26. +
      27. Contributing
      28. +
      29. License
      30. +
      31. Disclaimer
      32. + + +## Latest +`featurewiz` 5.0 version is out! It contains brand new Deep Learning Auto Encoders to enrich your data for the toughest imbalanced and multi-class datasets. If you are looking for the latest and greatest updates about our library, check out our updates page. +
        + +## Citation +If you use featurewiz in your research project or paper, please use the following format for citations: +

        +"Seshadri, Ram (2020). GitHub - AutoViML/featurewiz: Use advanced feature engineering strategies and select the best features from your data set fast with a single line of code. source code: https://github.com/AutoViML/featurewiz " +

        +Current citations for featurewiz + +[Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=) + +## Highlights +`featurewiz` stands out as a versatile and powerful tool for feature selection and engineering, capable of significantly enhancing model performance through intelligent feature transformation and selection techniques. Its unique methods like SULOV and recursive XGBoost, combined with advanced feature engineering options, make it a valuable addition to any data scientist's toolkit: +### Best Feature Selection Algorithm +

      33. It provides one of the best automatic feature selection algorithms (Minimum Redundancy Maximum Relevance (MRMR) algorithm) as described by wikipedia in this page: "The MRMR selection has been found to be more powerful than the maximum relevance feature selection" such as Boruta.
      34. + +### Advanced Feature Engineering Options +featurewiz extends beyond traditional feature selection by including powerful feature engineering capabilities such as: +
      35. Auto Encoders, including Denoising Auto Encoders (DAEs) and Variational Auto Encoders (VAEs), for improved model performance, especially on imbalanced datasets.
      36. +
      37. A variety of category encoders like HashingEncoder, SumEncoder, PolynomialEncoder, BackwardDifferenceEncoder, OneHotEncoder, HelmertEncoder, OrdinalEncoder, and BaseNEncoder.
      38. +
      39. The ability to add interaction features (e.g., x1x2, x2x3, x1^2), group by features, and target encoding.
      40. + +### SULOV Method for Feature Selection +
      41. SULOV stands for "Searching for Uncorrelated List Of Variables". It selects features that are uncorrelated with each other but have high correlation with the target variable, based on the Minimum Redundancy Maximum Relevance (mRMR) principle. This method effectively reduces redundancy in features while retaining those with high relevance to the target.
      42. + +### Recursive XGBoost Method +
      43. After applying the SULOV method, featurewiz employs a recursive approach using XGBoost's feature importance. This process is repeated multiple times on subsets of data, combining and deduplicating selected features to identify the most impactful ones.
      44. + +### Comprehensive Encoding and Transformation +
      45. featurewiz allows for extensive customization in how features are encoded and transformed, making it highly adaptable to various types of data.
      46. +
      47. The ability to combine multiple encoding and transformation methods enhances its flexibility and effectiveness in feature engineering.
      48. + +### Used by PhD's and Researchers and actively maintained +
      49. featurewiz is used by researchers and PhD data scientists around the world: there are 64 citations for featurewiz since its release: + +[Google Scholar](https://scholar.google.com/scholar?hl=en&as_sdt=0%2C31&q=featurewiz&btnG=)
      50. +
      51. It's efficient in handling large datasets, making it suitable for a wide range of applications from small to big data scenarios.
      52. +
      53. It is well-documented, and it comes with a number of examples.
      54. +
      55. It is actively maintained, and it is regularly updated with new features and bug fixes.
      56. + +## Internals +`featurewiz` has two major internal modules. They are explained below. +### 1. Feature Engineering module +

        The first step is not absolutely necessary but it can be used to create new features that may or may not be helpful (be careful with automated feature engineering tools!).

        +One of the gaps in open-source AutoML tools and especially Auto_ViML has been the lack of feature engineering capabilities that high-powered competitions such as Kaggle required. The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables was difficult and sifting through those hundreds of new features to find the best features was difficult and left only to "experts" or "professionals". featurewiz was created to help you in this endeavor.
        +

        featurewiz now enables you to add hundreds of such features with a single line of code. Set the "feature_engg" flag to "interactions", "groupby" or "target" and featurewiz will select the best encoders for each of those options and create hundreds (perhaps thousands) of features in one go. Not only that, using the next step, featurewiz will sift through numerous such variables and find only the least correlated and most relevant features to your model. All in one step!.
        + +![feature_engg](images/feature_engg.jpg) + +### 2. Feature Selection module +

        The second step is Feature Selection. `featurewiz` uses the MRMR (Minimum Redundancy Maximum Relevance) algorithm as the basis for its feature selection.
        + Why perform Feature Selection? Once you have created 100's of new features, you still have three questions left to answer: +1. How do we interpret those newly created features? +2. Which of these features is important and which is useless? How many of them are highly correlated to each other causing redundancy? +3. Does the model overfit now on these new features and perform better or worse than before? +
        +All are very important questions and featurewiz answers them by using the SULOV method and Recursive XGBoost to reduce features in your dataset to the best "minimum optimal" features for the model.
        +

        SULOV: SULOV stands for `Searching for Uncorrelated List of Variables`. The SULOV algorithm is based on the Minimum-Redundancy-Maximum-Relevance (MRMR) algorithm explained in this article as one of the best feature selection methods. To understand how MRMR works and how it is different from `Boruta` and other feature selection methods, see the chart below. Here "Minimal Optimal" refers to MRMR (featurewiz) while "all-relevant" refers to Boruta.
        + +![MRMR_chart](images/MRMR.png) + +## Working +`featurewiz` performs feature selection in 2 steps. Each step is explained below. +The working of the `SULOV` algorithm is as follows: +

          +
        1. Find all the pairs of highly correlated variables exceeding a correlation threshold (say absolute(0.7)).
        2. +
        3. Then find their MIS score (Mutual Information Score) to the target variable. MIS is a non-parametric scoring method. So its suitable for all kinds of variables and target.
        4. +
        5. Now take each pair of correlated variables, then knock off the one with the lower MIS score.
        6. +
        7. What’s left is the ones with the highest Information scores and least correlation with each other.
        8. +
        + +![sulov](images/SULOV.jpg) + +The working of the Recursive XGBoost is as follows: +Once SULOV has selected variables that have high mutual information scores with the least correlation among them, featurewiz uses XGBoost to repeatedly find the best features among the remaining variables after SULOV. +
          +
        1. Select all variables in the data set and the full data split into train and valid sets.
        2. +
        3. Find top X features (could be 10) on train using valid for early stopping (to prevent over-fitting)
        4. +
        5. Then take the next set of vars and find top X
        6. +
        7. Do this 5 times. Combine all selected features and de-duplicate them.
        8. +
        + +![xgboost](images/xgboost.jpg) + +## Tips +Here are some additional tips for ML engineers and data scientists when using featurewiz: +
          +
        1. How to cross-validate your results: When you use featurewiz, we automatically perform multiple rounds of feature selection using permutations on the number of columns. However, you can perform feature selection using permutations of rows as follows in cross_validate using featurewiz. +
        2. Use multiple feature selection tools: It is a good idea to use multiple feature selection tools and compare the results. This will help you to get a better understanding of which features are most important for your data.
        3. +
        4. Don't forget to engineer new features: Feature selection is only one part of the process of building a good machine learning model. You should also spend time engineering your features to make them as informative as possible. This can involve things like creating new features, transforming existing features, and removing irrelevant features.
        5. +
        6. Don't overfit your model: It is important to avoid overfitting your model to the training data. Overfitting occurs when your model learns the noise in the training data, rather than the underlying signal. To avoid overfitting, you can use regularization techniques, such as lasso or elasticnet.
        7. +
        8. Start with a small number of features: When you are first starting out, it is a good idea to start with a small number of features. This will help you to avoid overfitting your model. As you become more experienced, you can experiment with adding more features.
        9. +
        + +## Install + +**Prerequisites:** +
          +
        1. featurewiz is built using xgboost, dask, numpy, pandas and matplotlib. It should run on most Python 3 Anaconda installations. You won't have to import any special libraries other than "dask", "XGBoost" and "networkx" library. Optionally, it uses LightGBM for fast modeling, which it installs automatically.
        2. +
        3. We use "networkx" library for charts and interpretability.
          But if you don't have these libraries, featurewiz will install those for you automatically.
        4. +
        +To install from source: + +``` +cd +git clone git@github.com:AutoViML/featurewiz.git +# or download and unzip https://github.com/AutoViML/featurewiz/archive/master.zip +conda create -n python=3.7 anaconda +conda activate # ON WINDOWS: `source activate ` +cd featurewiz +pip install -r requirements.txt +``` + +## Good News: You can install featurewiz on Colab and Kaggle easily in 2 steps! +As of June 2022, thanks to [arturdaraujo](https://github.com/arturdaraujo), featurewiz is now available on conda-forge. You can try:
        + +``` + conda install -c conda-forge featurewiz +``` + +### If the above conda install fails, you can try installing featurewiz this way: +#### Install featurewiz using git+
        + +``` +!pip install git+https://github.com/AutoViML/featurewiz.git +``` + +## Usage + +There are two ways to use featurewiz. +
          +
        1. The first way is the new way where you use scikit-learn's `fit and predict` syntax. It also includes the `lazytransformer` library that I created to transform datetime, NLP and categorical variables into numeric variables automatically. We recommend that you use it as the main syntax for all your future needs.
        2. + +``` +from featurewiz import FeatureWiz +fwiz = FeatureWiz(feature_engg = '', nrows=None, transform_target=True, scalers="std", + category_encoders="auto", add_missing=False, verbose=0) +X_train_selected, y_train = fwiz.fit_transform(X_train, y_train) +X_test_selected = fwiz.transform(X_test) +### get list of selected features ### +fwiz.features +``` + +
        3. The second way is the old way and this was the original syntax of featurewiz. It is still being used by thousands of researchers in the field. Hence it will continue to be maintained. However, it can be discontinued any time without notice. You can use it if you like it.
        4. + +``` +import featurewiz as fwiz +outputs = fwiz.featurewiz(dataname=train, target=target, corr_limit=0.70, verbose=2, sep=',', + header=0, test_data='',feature_engg='', category_encoders='', + dask_xgboost_flag=False, nrows=None, skip_sulov=False, skip_xgboost=False) +``` + +`outputs` is a tuple: There will always be two objects in output. It can vary: +- In the first case, it can be `features` and `trainm`: features is a list (of selected features) and trainm is the transformed dataframe (if you sent in train only) +- In the second case, it can be `trainm` and `testm`: It can be two transformed dataframes when you send in both test and train but with selected features. + +In both cases, the features and dataframes are ready for you to do further modeling. + +Featurewiz works on any multi-class, multi-label data Set. So you can have as many target labels as you want. +You don't have to tell Featurewiz whether it is a Regression or Classification problem. It will decide that automatically. + +## API + +**Input Arguments for NEW syntax** + + Parameters + ---------- + corr_limit : float, default=0.90 + The correlation limit to consider for feature selection. Features with correlations + above this limit may be excluded. + + verbose : int, default=0 + Level of verbosity in output messages. + + feature_engg : str or list, default='' + Specifies the feature engineering methods to apply, such as 'interactions', 'groupby', + and 'target'. + + category_encoders : str or list, default='' + Encoders for handling categorical variables. Supported encoders include 'onehot', + 'ordinal', 'hashing', 'count', 'catboost', 'target', 'glm', 'sum', 'woe', 'bdc', + 'loo', 'base', 'james', 'helmert', 'label', 'auto', etc. + + add_missing : bool, default=False + If True, adds indicators for missing values in the dataset. + + dask_xgboost_flag : bool, default=False + If set to True, enables the use of Dask for parallel computing with XGBoost. + + nrows : int or None, default=None + Limits the number of rows to process. + + skip_sulov : bool, default=False + If True, skips the application of the Super Learning Optimized (SULO) method in + feature selection. + + skip_xgboost : bool, default=False + If True, bypasses the recursive XGBoost feature selection. + + transform_target : bool, default=False + When True, transforms the target variable(s) into numeric format if they are not + already. + + scalers : str or None, default=None + Specifies the scaler to use for feature scaling. Available options include + 'std', 'standard', 'minmax', 'max', 'robust', 'maxabs'. + +**Input Arguments for old syntax** + +- `dataname`: could be a datapath+filename or a dataframe. It will detect whether your input is a filename or a dataframe and load it automatically. +- `target`: name of the target variable in the data set. +- `corr_limit`: if you want to set your own threshold for removing variables as highly correlated, then give it here. The default is 0.9 which means variables less than -0.9 and greater than 0.9 in pearson's correlation will be candidates for removal. +- `verbose`: This has 3 possible states: + - `0` - limited output. Great for running this silently and getting fast results. + - `1` - verbose. Great for knowing how results were and making changes to flags in input. + - `2` - more charts such as SULOV and output. Great for finding out what happens under the hood for SULOV method. +- `test_data`: This is only applicable to the old syntax if you want to transform both train and test data at the same time in the same way. `test_data` could be the name of a datapath+filename or a dataframe. featurewiz will detect whether your input is a filename or a dataframe and load it automatically. Default is empty string. +- `dask_xgboost_flag`: default False. If you want to use dask with your data, then set this to True. +- `feature_engg`: You can let featurewiz select its best encoders for your data set by setting this flag + for adding feature engineering. There are three choices. You can choose one, two, or all three. + - `interactions`: This will add interaction features to your data such as x1*x2, x2*x3, x1**2, x2**2, etc. + - `groupby`: This will generate Group By features to your numeric vars by grouping all categorical vars. + - `target`: This will encode and transform all your categorical features using certain target encoders.
          + Default is empty string (which means no additional features) +- `add_missing`: default is False. This is a new flag: the `add_missing` flag will add a new column for missing values for all your variables in your dataset. This will help you catch missing values as an added signal. +- `category_encoders`: default is "auto". Instead, you can choose your own category encoders from the list below. + We recommend you do not use more than two of these. Featurewiz will automatically select only two if you have more than two in your list. You can set "auto" for our own choice or the empty string "" (which means no encoding of your categorical features)
          These descriptions are derived from the excellent category_encoders python library. Please check it out! + - `HashingEncoder`: HashingEncoder is a multivariate hashing implementation with configurable dimensionality/precision. The advantage of this encoder is that it does not maintain a dictionary of observed categories. Consequently, the encoder does not grow in size and accepts new values during data scoring by design. + - `SumEncoder`: SumEncoder is a Sum contrast coding for the encoding of categorical features. + - `PolynomialEncoder`: PolynomialEncoder is a Polynomial contrast coding for the encoding of categorical features. + - `BackwardDifferenceEncoder`: BackwardDifferenceEncoder is a Backward difference contrast coding for encoding categorical variables. + - `OneHotEncoder`: OneHotEncoder is the traditional Onehot (or dummy) coding for categorical features. It produces one feature per category, each being a binary. + - `HelmertEncoder`: HelmertEncoder uses the Helmert contrast coding for encoding categorical features. + - `OrdinalEncoder`: OrdinalEncoder uses Ordinal encoding to designate a single column of integers to represent the categories in your data. Integers however start in the same order in which the categories are found in your dataset. If you want to change the order, just sort the column and send it in for encoding. + - `FrequencyEncoder`: FrequencyEncoder is a count encoding technique for categorical features. For a given categorical feature, it replaces the names of the categories with the group counts of each category. + - `BaseNEncoder`: BaseNEncoder encodes the categories into arrays of their base-N representation. A base of 1 is equivalent to one-hot encoding (not really base-1, but useful), and a base of 2 is equivalent to binary encoding. N=number of actual categories is equivalent to vanilla ordinal encoding. + - `TargetEncoder`: TargetEncoder performs Target encoding for categorical features. It supports the following kinds of targets: binary and continuous. For multi-class targets, it uses a PolynomialWrapper. + - `CatBoostEncoder`: CatBoostEncoder performs CatBoost coding for categorical features. It supports the following kinds of targets: binary and continuous. For polynomial target support, it uses a PolynomialWrapper. This is very similar to leave-one-out encoding, but calculates the values “on-the-fly”. Consequently, the values naturally vary during the training phase and it is not necessary to add random noise. + - `WOEEncoder`: WOEEncoder uses the Weight of Evidence technique for categorical features. It supports only one kind of target: binary. For polynomial target support, it uses a PolynomialWrapper. It cannot be used for Regression. + - `JamesSteinEncoder`: JamesSteinEncoder uses the James-Stein estimator. It supports 2 kinds of targets: binary and continuous. For polynomial target support, it uses PolynomialWrapper. + For feature value i, James-Stein estimator returns a weighted average of: + The mean target value for the observed feature value i. + The mean target value (regardless of the feature value). +- `nrows`: default `None`. You can set the number of rows to read from your datafile if it is too large to fit into either dask or pandas. But you won't have to if you use dask. +- `skip_sulov`: default `False`. You can set the flag to skip the SULOV method if you want. +- `skip_xgboost`: default `False`. You can set the flag to skip the Recursive XGBoost method if you want. + +**Output values for old syntax** This applies only to the old syntax. +- `outputs`: Output is always a tuple. We can call our outputs in that tuple as `out1` and `out2` below. + - `out1` and `out2`: If you sent in just one dataframe or filename as input, you will get: + - 1. `features`: It will be a list (of selected features) and + - 2. `trainm`: It will be a dataframe (if you sent in a file or dataname as input) + - `out1` and `out2`: If you sent in two files or dataframes (train and test), you will get: + - 1. `trainm`: a modified train dataframe with engineered and selected features from dataname and + - 2. `testm`: a modified test dataframe with engineered and selected features from test_data. + +## Additional +To learn more about how featurewiz works under the hood, watch this [video](https://www.youtube.com/embed/ZiNutwPcAU0) + +![background](images/featurewiz_background.jpg) + +featurewiz was designed for selecting High Performance variables with the fewest steps. +In most cases, featurewiz builds models with 20%-99% fewer features than your original data set with nearly the same or slightly lower performance (this is based on my trials. Your experience may vary).
          +

          +featurewiz is every Data Scientist's feature wizard that will:

            +
          1. Automatically pre-process data: you can send in your entire dataframe "as is" and featurewiz will classify and change/label encode categorical variables changes to help XGBoost processing. It classifies variables as numeric or categorical or NLP or date-time variables automatically so it can use them correctly to model.
            +
          2. Perform feature engineering automatically: The ability to create "interaction" variables or adding "group-by" features or "target-encoding" categorical variables is difficult and sifting through those hundreds of new features is painstaking and left only to "experts". Now, with featurewiz you can create hundreds or even thousands of new features with the click of a mouse. This is very helpful when you have a small number of features to start with. However, be careful with this option. You can very easily create a monster with this option. +
          3. Perform feature reduction automatically. When you have small data sets and you know your domain well, it is easy to perhaps do EDA and identify which variables are important. But when you have a very large data set with hundreds if not thousands of variables, selecting the best features from your model can mean the difference between a bloated and highly complex model or a simple model with the fewest and most information-rich features. featurewiz uses XGBoost repeatedly to perform feature selection. You must try it on your large data sets and compare!
            +
          4. Explain SULOV method graphically using networkx library so you can see which variables are highly correlated to which ones and which of those have high or low mutual information scores automatically. Just set verbose = 2 to see the graph.
            +
          5. Build a fast XGBoost or LightGBM model using the features selected by featurewiz. There is a function called "simple_lightgbm_model" which you can use to build a fast model. It is a new module, so check it out.
            +
          + +*** Special thanks to fellow open source Contributors ***:
          +
            +
          1. Alex Lekov (https://github.com/Alex-Lekov/AutoML_Alex/tree/master/automl_alex) for his DataBunch and encoders modules which are used by the tool (although with some modifications).
          2. +
          3. Category Encoders library in Python : This is an amazing library. Make sure you read all about the encoders that featurewiz uses here: https://contrib.scikit-learn.org/category_encoders/index.html
          4. +
          + +## Maintainers + +* [@AutoViML](https://github.com/AutoViML) + +## Contributing + +See [the contributing file](CONTRIBUTING.md)! + +PRs accepted. + +## License + +Apache License 2.0 © 2020 Ram Seshadri + +## DISCLAIMER +This project is not an official Google project. It is not supported by Google and Google specifically disclaims all warranties as to its quality, merchantability, or fitness for a particular purpose. + + +[page]: examples/cross_validate.py

      }8MM=H-;)M3j5$`S)kDTG2IzWW{_*-#Lg&GKk_o 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 literal 0 HcmV?d00001 diff --git a/dist/featurewiz-0.5.0.tar.gz b/dist/featurewiz-0.5.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..648e24308d60323600380e4e4e51559ac2041d44 GIT binary patch literal 130300 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