From b0609e1ad1a022da247e5c72305bfc8daa949184 Mon Sep 17 00:00:00 2001 From: haotian <2421912570@qq.com> Date: Thu, 20 Feb 2025 10:37:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90--=E5=AE=8C=E6=88=90=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E6=A8=A1=E5=9E=8B=E9=A2=84=E6=B5=8B=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/接口文档code.md | 41 +++- example_model_delete.py | 6 + example_model_manager.py | 2 +- example_model_predict.py | 8 + example_model_trainer.py | 2 +- .../__pycache__/model_manager.cpython-39.pyc | Bin 6214 -> 9831 bytes function/model_manager.py | 188 +++++++++++++++++- .../artifacts/model/MLmodel | 20 ++ .../artifacts/model/conda.yaml | 15 ++ .../artifacts/model/model.pkl | Bin 0 -> 96534 bytes .../artifacts/model/python_env.yaml | 7 + .../artifacts/model/requirements.txt | 8 + .../artifacts/model/MLmodel | 20 ++ .../artifacts/model/conda.yaml | 15 ++ .../artifacts/model/model.pkl | Bin 0 -> 96534 bytes .../artifacts/model/python_env.yaml | 7 + .../artifacts/model/requirements.txt | 8 + .../artifacts/model/MLmodel | 20 ++ .../artifacts/model/conda.yaml | 15 ++ .../artifacts/model/model.pkl | Bin 0 -> 96534 bytes .../artifacts/model/python_env.yaml | 7 + .../artifacts/model/requirements.txt | 8 + .../meta.yaml | 15 ++ .../metrics/accuracy | 1 + .../metrics/f1 | 1 + .../metrics/precision | 1 + .../metrics/recall | 1 + .../metrics/roc_auc | 1 + .../params/advantages | 1 + .../params/algorithm | 1 + .../params/dataset | 1 + .../params/disadvantages | 1 + .../params/learning_rate | 1 + .../params/max_depth | 1 + .../params/n_estimators | 1 + .../params/principle | 1 + .../params/random_state | 1 + .../params/task_type | 1 + .../tags/mlflow.log-model.history | 1 + .../tags/mlflow.runName | 1 + .../tags/mlflow.source.git.commit | 1 + .../tags/mlflow.source.name | 1 + .../tags/mlflow.source.type | 1 + .../tags/mlflow.user | 1 + mlruns/189649205577051698/meta.yaml | 6 + .../meta.yaml | 15 ++ .../metrics/accuracy | 1 + .../metrics/f1 | 1 + .../metrics/precision | 1 + .../metrics/recall | 1 + .../metrics/roc_auc | 1 + .../params/advantages | 1 + .../params/algorithm | 1 + .../params/dataset | 1 + .../params/disadvantages | 1 + .../params/learning_rate | 1 + .../params/max_depth | 1 + .../params/n_estimators | 1 + .../params/principle | 1 + .../params/random_state | 1 + .../params/task_type | 1 + .../tags/mlflow.log-model.history | 1 + .../tags/mlflow.runName | 1 + .../tags/mlflow.source.git.commit | 1 + .../tags/mlflow.source.name | 1 + .../tags/mlflow.source.type | 1 + .../tags/mlflow.user | 1 + .../meta.yaml | 15 ++ .../metrics/accuracy | 1 + .../metrics/f1 | 1 + .../metrics/precision | 1 + .../metrics/recall | 1 + .../metrics/roc_auc | 1 + .../params/advantages | 1 + .../params/algorithm | 1 + .../params/dataset | 1 + .../params/disadvantages | 1 + .../params/learning_rate | 1 + .../params/max_depth | 1 + .../params/n_estimators | 1 + .../params/principle | 1 + .../params/random_state | 1 + .../params/task_type | 1 + .../tags/mlflow.log-model.history | 1 + .../tags/mlflow.runName | 1 + .../tags/mlflow.source.git.commit | 1 + .../tags/mlflow.source.name | 1 + .../tags/mlflow.source.type | 1 + .../tags/mlflow.user | 1 + .../pred_breast_cancer_20250219_145614.csv | 104 ++++++++++ test_model_manager.py | 99 +++++++++ 91 files changed, 700 insertions(+), 14 deletions(-) create mode 100644 example_model_delete.py create mode 100644 example_model_predict.py create mode 100644 mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/MLmodel create mode 100644 mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/conda.yaml create mode 100644 mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/model.pkl create mode 100644 mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/python_env.yaml create mode 100644 mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/requirements.txt create mode 100644 mlartifacts/433321862082712659/d16c356bb3324ede819f9998d427780a/artifacts/model/MLmodel create mode 100644 mlartifacts/433321862082712659/d16c356bb3324ede819f9998d427780a/artifacts/model/conda.yaml create mode 100644 mlartifacts/433321862082712659/d16c356bb3324ede819f9998d427780a/artifacts/model/model.pkl create mode 100644 mlartifacts/433321862082712659/d16c356bb3324ede819f9998d427780a/artifacts/model/python_env.yaml create mode 100644 mlartifacts/433321862082712659/d16c356bb3324ede819f9998d427780a/artifacts/model/requirements.txt create mode 100644 mlartifacts/656341556838275234/81b3390084a146bb949fec21c80dba2c/artifacts/model/MLmodel create mode 100644 mlartifacts/656341556838275234/81b3390084a146bb949fec21c80dba2c/artifacts/model/conda.yaml create mode 100644 mlartifacts/656341556838275234/81b3390084a146bb949fec21c80dba2c/artifacts/model/model.pkl create mode 100644 mlartifacts/656341556838275234/81b3390084a146bb949fec21c80dba2c/artifacts/model/python_env.yaml create mode 100644 mlartifacts/656341556838275234/81b3390084a146bb949fec21c80dba2c/artifacts/model/requirements.txt create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/meta.yaml create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/metrics/accuracy create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/metrics/f1 create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/metrics/precision create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/metrics/recall create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/metrics/roc_auc create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/advantages create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/algorithm create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/dataset create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/disadvantages create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/learning_rate create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/max_depth create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/n_estimators create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/principle create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/random_state create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/params/task_type create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.log-model.history create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.runName create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.source.git.commit create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.source.name create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.source.type create mode 100644 mlruns/189649205577051698/725c551dce9c477391856e6ac41c75bf/tags/mlflow.user create mode 100644 mlruns/189649205577051698/meta.yaml create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/meta.yaml create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/metrics/accuracy create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/metrics/f1 create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/metrics/precision create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/metrics/recall create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/metrics/roc_auc create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/advantages create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/algorithm create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/dataset create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/disadvantages create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/learning_rate create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/max_depth create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/n_estimators create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/principle create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/random_state create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/params/task_type create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.log-model.history create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.runName create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.source.git.commit create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.source.name create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.source.type create mode 100644 mlruns/433321862082712659/d16c356bb3324ede819f9998d427780a/tags/mlflow.user create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/meta.yaml create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/metrics/accuracy create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/metrics/f1 create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/metrics/precision create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/metrics/recall create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/metrics/roc_auc create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/advantages create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/algorithm create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/dataset create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/disadvantages create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/learning_rate create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/max_depth create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/n_estimators create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/principle create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/random_state create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/params/task_type create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.log-model.history create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.runName create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.source.git.commit create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.source.name create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.source.type create mode 100644 mlruns/656341556838275234/81b3390084a146bb949fec21c80dba2c/tags/mlflow.user create mode 100644 predictions/pred_breast_cancer_20250219_145614.csv create mode 100644 test_model_manager.py diff --git a/doc/接口文档code.md b/doc/接口文档code.md index f2c2e0e..4d29cd9 100644 --- a/doc/接口文档code.md +++ b/doc/接口文档code.md @@ -500,26 +500,49 @@ Response: } ``` -### 2.8 模型预测 +### 2.9 模型预测 ```http -POST /api/predict +POST /api/model/predict Content-Type: application/json Request: { - "model_id": "model_20230820_001", + "run_id": "7970364d490f4e0aa0375c2db26215f3", "data": "dataset/dataset_processed/test.csv", - "output_path": "predictions/pred_20230820_001.csv" + "output_path": "predictions/pred_20250219_001.csv", + "batch_size": 32, + "device": "cuda", + "return_proba": true, + "metrics": ["accuracy", "f1", "precision", "recall"] } Response: { "status": "success", - "prediction_id": "pred_20230820_001", - "output_file": "predictions/pred_20230820_001.csv", - "metrics": { - "accuracy": 0.95, - "f1": 0.94 + "prediction": { + "id": "pred_20250219_001", + "run_id": "7970364d490f4e0aa0375c2db26215f3", + "model_name": "XGBClassifier", + "output_file": "predictions/pred_20250219_001.csv", + "prediction_time": "2025-02-19 15:30:45", + "samples_count": 1000, + "metrics": { + "accuracy": 0.956, + "f1": 0.948, + "precision": 0.962, + "recall": 0.935 + }, + "execution_time": "5.23s" + } +} + +Error Response: +{ + "status": "error", + "message": "模型预测失败", + "details": { + "error_type": "ValueError", + "error_message": "输入数据格式不正确" } } ``` diff --git a/example_model_delete.py b/example_model_delete.py new file mode 100644 index 0000000..4dc9e98 --- /dev/null +++ b/example_model_delete.py @@ -0,0 +1,6 @@ +from function.model_manager import ModelManager + +# 创建模型管理器实例 +manager = ModelManager() +back = manager.delete_model('7970364d490f4e0aa0375c2db26215f3') +print(back) \ No newline at end of file diff --git a/example_model_manager.py b/example_model_manager.py index a4791ab..f140197 100644 --- a/example_model_manager.py +++ b/example_model_manager.py @@ -7,7 +7,7 @@ manager = ModelManager() result = manager.get_finished_models( page=1, page_size=10, - experiment_name='breast_cancer_classification_2' + experiment_name='breast_cancer_classification_3' ) # 打印结果 diff --git a/example_model_predict.py b/example_model_predict.py new file mode 100644 index 0000000..68bcff2 --- /dev/null +++ b/example_model_predict.py @@ -0,0 +1,8 @@ +from function.model_manager import ModelManager + +model_manager = ModelManager() + +print(model_manager.predict(run_id = "33939ea6d8ce4d43a268f23f7361651e",\ + data_path="/home/admin-root/haotian/MLPlatform/dataset/dataset_processed/breast_cancer_20250219_145614/test_breast_cancer_20250219_145614.csv",\ + output_path="predictions/pred_breast_cancer_20250219_145614.csv" ,\ + metrics= ["accuracy", "f1", "precision", "recall"] )) \ No newline at end of file diff --git a/example_model_trainer.py b/example_model_trainer.py index 285ad9f..ed0bc7b 100644 --- a/example_model_trainer.py +++ b/example_model_trainer.py @@ -30,7 +30,7 @@ model_config = { # 训练模型, 删除训练实验时要删除 mlruns/.trash/ 回收站里的文件 # 模型文件 直接在 mlruns/文件夹下 -for i in range(8, 20): +for i in range(3, 4): result = trainer.train_model( { 'features': X_train, diff --git a/function/__pycache__/model_manager.cpython-39.pyc b/function/__pycache__/model_manager.cpython-39.pyc index 16955e8da9b19858092667f078ad1e6306eb401b..23d59e7e2931cf7f8afabff626bc5d1e6566107a 100644 GIT binary patch delta 5322 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 PpcR7_{tidLUBv$dD(Aka delta 1753 zcmZ{l&vO(-6vwA$W@jh+V|Md9OO}{~Kr$ghz+aJsn6e}x2mya0gECCEhp=IGHq|>z z5enr%s>ELuRA1CeA*te^lt0jvRmwqc`~{YWK6x+!tMuZ{;-K&KA^}QwYChBby64U7 z?|a>QYwyiL%S)#d3jFST@$Jl+O_!`Z2`_ElW0N4|q^Lqkkap7YX*t$}LSxieP^fWE zbuyH^q}Xw8ktYWV=;8jK0i7|pE5`x8;t;AhDkY9aoA_I5>#k-$4wvcIDwU}pxFs+2W_;$J=3mEN&ZCr0 za=aM$yS(@j?369TkRC8bnc+7(0T5crJH?J*_jSY@h(n0oh@*%}#2&=+h_!^& zKb(-)6e!I5;@Xe%5Ae3;-mFw;3i~eN2;bk_-2VzTUqw8mT?P~4pPKU-6j2yeKHwLc z=M7nU9n0yKpUDuev)T+;UA(sX&JQc+uiyUh;_apPC!hQEyBnQV+*h5qK3-V;bZO=C zdr}ZtuMTf-8+u$04|LWBcXTpTwTdCsEGy@){Pxk+)$P8>tUy3>o{v2smb)LrPXkv;YzG#MTfj)mpm{+rl}lvU_+UUVEzFy&d{=&@z{6aw_kZO~-SsakTktP)hr4Vt1&fg-xelB? z&>6QamgHkS0|)VBk>`xiim*{**p)?a5L!Z?E|&u~jm8%!1h3A{}q5RM_|>}z#ql^R>8pFpd=<~f*YT3ss8{? Dict: + """ + 使用指定的模型进行预测 + + Args: + run_id: MLflow运行ID + data_path: 输入数据路径 + output_path: 预测结果保存路径 + batch_size: 批处理大小 + device: 计算设备 ('cuda' or 'cpu') + return_proba: 是否返回概率预测 + metrics: 评估指标列表 + + Returns: + 预测结果信息 + """ + # try: + start_time = time.time() + + # 获取模型信息 + run = self.client.get_run(run_id) + if not run: + return { + 'status': 'error', + 'message': f'未找到运行ID为 {run_id} 的模型' + } + + # 加载模型 + model = mlflow.pyfunc.load_model(f"runs:/{run_id}/model") + model_name = run.data.params.get('algorithm', 'Unknown') + + # 加载数据 + try: + data = pd.read_csv(data_path) + if 'label' in data.columns: + y_true = data.pop('label').values + has_labels = True + elif 'target' in data.columns: + y_true = data.pop('target').values + has_labels = True + else: + has_labels = False + X = data.values + except Exception as e: + return { + 'status': 'error', + 'message': '数据加载失败', + 'details': { + 'error_type': type(e).__name__, + 'error_message': str(e) + } + } + + # 创建预测ID + pred_id = f"pred_{datetime.now():%Y%m%d_%H%M%S}" + + # 进行预测 + if isinstance(model, torch.nn.Module): + # PyTorch模型预测 + model.to(device) + model.eval() + + dataset = TensorDataset(torch.FloatTensor(X)) + dataloader = DataLoader(dataset, batch_size=batch_size) + + predictions = [] + probas = [] + + with torch.no_grad(): + for batch in dataloader: + batch = batch[0].to(device) + outputs = model(batch) + + if return_proba: + proba = torch.softmax(outputs, dim=1) + probas.append(proba.cpu().numpy()) + + preds = outputs.argmax(dim=1) + predictions.append(preds.cpu().numpy()) + + predictions = np.concatenate(predictions) + if return_proba: + probas = np.concatenate(probas) + else: + # 其他模型预测 + predictions = model.predict(X) + if return_proba and hasattr(model, 'predict_proba'): + probas = model.predict_proba(X) + else: + probas = [] + + # 计算评估指标 + metrics_results = {} + if has_labels and metrics: + for metric in metrics: + if metric in self.metrics_map.keys(): + metrics_results[metric] = float(self.metrics_map[metric](y_true, predictions)) + + # 保存预测结果 + results_df = pd.DataFrame({ + 'prediction': predictions + }) + if return_proba and len(probas) > 0: + for i in range(probas.shape[1]): + results_df[f'probability_{i}'] = probas[:, i] + + # 确保输出目录存在 + os.makedirs(os.path.dirname(output_path), exist_ok=True) + results_df.to_csv(output_path, index=False) + + # 计算执行时间 + execution_time = time.time() - start_time + + # 记录日志 + self.logger.info( + f"预测完成 - Run ID: {run_id}, 模型: {model_name}, " + f"样本数: {len(predictions)}, 耗时: {execution_time:.2f}s" + ) + + return { + 'status': 'success', + 'prediction': { + 'id': pred_id, + 'run_id': run_id, + 'model_name': model_name, + 'output_file': output_path, + 'prediction_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'samples_count': len(predictions), + 'metrics': metrics_results, + 'execution_time': f"{execution_time:.2f}s" + } + } + + # except Exception as e: + # error_msg = f"预测过程发生错误: {str(e)}" + # self.logger.error(error_msg) + # return { + # 'status': 'error', + # 'message': '模型预测失败', + # 'details': { + # 'error_type': type(e).__name__, + # 'error_message': str(e) + # } + # } \ No newline at end of file diff --git a/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/MLmodel b/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/MLmodel new file mode 100644 index 0000000..d646df5 --- /dev/null +++ b/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/MLmodel @@ -0,0 +1,20 @@ +artifact_path: model +flavors: + python_function: + env: + conda: conda.yaml + virtualenv: python_env.yaml + loader_module: mlflow.sklearn + model_path: model.pkl + predict_fn: predict + python_version: 3.9.19 + sklearn: + code: null + pickled_model: model.pkl + serialization_format: cloudpickle + sklearn_version: 1.5.2 +mlflow_version: 2.20.1 +model_size_bytes: 96534 +model_uuid: 80ec24f7f34643b9be9da8be761430eb +run_id: 725c551dce9c477391856e6ac41c75bf +utc_time_created: '2025-02-20 01:43:39.758338' diff --git a/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/conda.yaml b/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/conda.yaml new file mode 100644 index 0000000..306a2fe --- /dev/null +++ b/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/conda.yaml @@ -0,0 +1,15 @@ +channels: +- conda-forge +dependencies: +- python=3.9.19 +- pip<=24.0 +- pip: + - mlflow==2.20.1 + - cloudpickle==3.1.0 + - numpy==1.26.4 + - pandas==2.2.2 + - psutil==6.0.0 + - scikit-learn==1.5.2 + - scipy==1.13.1 + - xgboost==2.1.4 +name: mlflow-env diff --git a/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/model.pkl b/mlartifacts/189649205577051698/725c551dce9c477391856e6ac41c75bf/artifacts/model/model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..77a11d53cccdc98a598e8f34458de8dc0f1c2c42 GIT binary patch literal 96534 zcmeHQ2Uruy+oy+KRK(sr%h|;S$<8eHdiJg%K!8;sn1H>2UB*R$)LdbYEDJAs*mkPRp?=lXoZ6Shuy-+6y;o6RP{VQyo48(m598Xgg%P^jYS zs0K&LWy+Ye6p!AmgPKRlR4OK%kt@^Yq}51F8l z)!=sBB9w|@(pW_l6FP!B^w150iHVRZWpVPf)?oJnR2HVV43#TG6e=bT-6hfunHwBe zKTau^OQYp+k%}IX=o%96($`fXCflwxO1M_LyC%%NmX(3SniCA zDn3Ldi;j(wa|d3!J=8a8&O1~Qm3>_rG6MC)-R5I>E=oRB9>tx^bz_Vo4BhCWlt)Nq zQL&L|Jg(>6qR2C)Y$X4k`=NckY$)&1Lg%VX3MKjzC#6+aE9``zDs(fPN zqouJjr7S8+9wp5gjk{776Q+pf2txd7Uo*uiMmk6lg1B5PS`nj&Q^d%np^6w)oKnWb z#G&(k+*YoXaWkjjSd{CM7ZV+;P{zq(Lgmu95wU0>PGRz)OeoqbI#d?LgmK;I2Ep-2 z&dEWilp^vNSqMi=D0d}7p=3D9(x#+%gv(`d@yhIG&}}}5`P@CyIE9oOQ=|gTi2h`x zQm%?rM4>tKjE;|rV>pVC2t=Uyc9U~mqgmlv=MH^2L5&*0-Jpn#)rpECJ|+zH;llMD z6(S2Aj1F8m;*c22RnoNfb}7!%ASCCU(4@Fynb+|;MT9Tph7a3+Eri*zz9SCSJR5%L&0H}q`2k<&Sx z?S|X24~>n-`^Cd$Occi_DHEgPx-=p-Udn`#du{9CJ9FmS#1&CY9LH6esYopbOGixl+m?Awn`l>gxw(N>uc>)gObq@s3c+!Wel>oHPf~ggHeb zLb8#~VJXUpae0AY&^B(`1)&8sp&}RU>({GCy2c&oaADMed#q9sDp#p=7$DU}Ve)Vp zCz`kiL)AlWa*)NxDexVl_*jG&CAsRS({@ARWifFK2boAtS4PVQ%ayo-TTTV3N6ES4$pu?7hTP_i++b?B*d~&j>>D&{jC;tdC&_Jgjs5f9g;gxM%a+_^ zWCZnh5RIa@-1>`TBnF0Df0b0tV|DfS5T1A<%dCoxV&bGx3Y8w6`Ux^=xJXNO)oWma z-IeliWH*p!5l(KM*5fd7zRKzeN4PZsMQY_m{b081XsV6CNX2=*E8#ijyurZI~;Zts7 z$o)lgETc5qgjP53e@K6>jqCWME?d?noGGHS6w+`d=k##yq*4~4AD+R?Bkiv<|D-Lw ze7T{EIsOT`Y}b(Rz*T2DWXcG+p_a=_g()Uu*Oattq>f96F)E$Alw(Y!2CfaMqaUh~ zFGTJrirhLyd|YgNoX!{{C5dH0@0XL?joouXizwTvQkjyZ+-60@x%LRJq^iCfB73=O z7mLT!%;XOi>eUDjW-4{msI0CMMQ4}xl*an`!OCl1RI_9 zA`}~4a782=o%7byY;=Y5BM8w(m%IqoMi+`@U%+G=UG-Gu&~0?yFAw2HH@FYug55ad zN%zP;Ydq^-*{2B;2oqhiFXWnc@9cAWCXl;*vac90fn3VzfiQtwD2D5poO{olzKkc| zFZ+BRCSarzOrTiMBqkigqZBf|mi+%ip>P9FF@FT+s58<;i{g06lBRiNbY{9R zU72o7ccur^lj+6uX8JIFnSM-vW&ksgkut%Ij0s^vnJ~IT~H>cvaoH(Lr98%v9#qkm`=FeyBli(~G*NIf z?#M~wnvumspaB|iwz+@nzv!(y>dw+-w@mLlEP)JE*k$J32%DT~HC)7jhqPVsA1%eSv3?`$1Q*DX|0TO{XSM(l?r5 zM;E?u4lb8~)u-t6Awz!AjV>{UqB^~AXxj|;=Z)lA7fwrua=Cp2mk;^iWC&*9oAG`) zm@hlI$G(&^*j)Qh_FacLlKxSDYnYIWRFMX!1AlCF1b6l=z}FTN^X1{|b!HX4=;y7p ztl=J5qQcTV63U&xgApiv%?Zob1K$Y0?m@*~uBTVhorB!Q+=PB9tH9B~OZ5B=vxA)M zpMg!3C!Pma!NGT4;2SVwz+vs(Oa`p+xy0US(@4^#yGYaVP6p+sQ3j@U{-)q-hiB@G z!#C*R;*#{#fX%eOZyV^E_3O6?Uo*n;^}ttl^+k}!!(H_1JHvw<8h3-Gdv*o~H(sDK zrcI;YkKPY9et=jgga%(X@$|ZV!Q_!;wPzCC!Lr)pwaFC%C9Ups*926VK{;Ju#kCyD zvEJ(5?8ieS@g*!P>}G%)JWa@rCXOdRkWtlKZZvo7V#@bK=WyG+?zCIbfwh!H66q{b(@=?Sn&fM4baft#`D6R&Rg&RdE9PP7Z-dhpX2Qo)K~dB$SLJ1 zz50YF{bryPmc3jQ?5jMV&RBbneiZ8iPTjr(e&o(^AKaLARDfN5vc6GR58OjMa#91J zT(wPmChQ5A=R8ZB(6g)h+{96uy3@2^$%#7ZnZ3MNTi?p;lfCUIJD<)HhjBv)wfa!l zFhC-H<<}KZ=Z@1b4M-ahobYP{RGC*BaM#?-!G%{Vcm%DUdxcK&nF-Fl4~7H#drS7a z?!f{|wGPI+xwd%ZrnG~{IBpyUcbnA?+w^}UyqM`$A{rqpRcwuFFn&cFHZ{}EV)Oj#S+W^4ZfMxXc4t+s# zP$S%^we(0s++kt$h?ls<%mZNdmTB4r&nke0Ef1(OnpXoAy4Tfay?iKMJ9Dz+{mYw@ z%{GSv>dU+W&p+LxJ+w83?dtl6rs^&aJP2X=7G8Q;(CuL=kVbLD2mBfZRnMhS5QMmM zD1B#An{eknFwAiCyqd);Xhx(nEPlQ--LLJFm;fFPgxWSN_;x-WySW|NK8Zd8LGxhlS&(-_)4QDUAUz5BX;wHY9p(50NBR-82CWv1H zVVg)63OLas5e~O+QVZc`c{Ptm&@k^G=Q*RXaNEjtZDGEZi_j~qftK9r=iqp`tRdg>68tJ z=!+Q*Af5CcEV|v39)B&7K2dosI6Qm-m~ngqcrNZh@4xB?lKYe(Bd|(#)2_b7d>RjN ziJEV~ltY`elc&A}v;KEf-7NZ*B&Dsr_9uCgIHBS=$%^J+2Wu9| z<-t%J)%X&LJPV^ou*ZTt4p%%(Gad_qP#%vZhumD56z)Fa!QghydMX~sQvsE>+i3^> z7^L%5as*J*?R_;v0n~XYMgnMFk01{Pxo1d!80rn=u^5(udM2N$NBmcI_mB#hQJ(5TD%Vu5u?ML;TkHPHzGrtik1*JbsCDu?J zRx*Mod|j@8x7Wv+5#4=+6NYj7mpUETnu-WRJw)B6F} z53{ukYTlIGc^E97aiFebaY%IL!tLwCO%F}cu8`MeYbDpwIM3fls1}q?;0sofUU0!A z@#_Ut{Y&OYzU!I{Zr)0uZ62KfUcDvTe*sx6(-1?5LfiK@=~FcMXMJ!707anIo@P9$L_8*U5gh46_SpYHJr zs<7_{{o`+8^onf*=(#gDg5=VXutJC(J!5qlI@M1CCe8N+nh%*E@nAcUSmQW7DJ=mU zA3uhU-?bS_fwlY*3+6{$+ogjRT~=z--_8M}M~~Kuf0?ab=(t~FJE}XF>T*~;XUuA~ zY+zM3GqAVh=9!V=C+Wl)5>}4Zv|EZa5b^B!H4v(tO9OGoB%a~+Ni2iC6ST!r!Rw)T z1o^CdK)WAV1$wRuhL!x&sqyD8VqvF>G{L*MwzySO8s}Tdk6;1Z*J_420p!;#Xuizl zM+|c%gu9rtJf4hc0pimq9zlMSp3qraHqqhgzVxbwC*b#gl>l=Gm!Z3RzN8nf|-zh}P2rGp@_E^BxvwE7d*6zDpQJs1=kiV_FQ59&y72eS#jr`hpWeqEFB{ z?wIHl+^iWrV%xSA&^-GUUKqL$6zR7Vi!?Z6>y7u(mg;bY9O70Bqes57{0Q|=kIg7Q zqVrU81Q6v%3NG{Ai9C-R!RY@JkM(lb>l8VBLMfh|8dHltC zI^cw&7Mxdi9<1Yh8H}G*0{n4EY>0cqeJoJ|)d7;blPaR;QQ3z}XRvLit=Br8&&W@# z%g6A@={OzjOPjP5E8Rx+V>0@2}BEPlECX_~QxkWgvcSfTfPCm{=yBpnqxl z0?Z!i4OgvO1#I4J#)1q|j~>DMXe-{jh#X=WH>Dj4nwR*>;t5bXXaR~RsKI^4VgU?X z(Ccz80kr>^Xe@yIS_93O;d-k(VPD>O0zI~`E10u9+QYN}@#zl_`oWfcbpPm0^zaww z==Puqn4;Pb{yF!O?%S_89T3?AOx)=Oc31nCZr<1rOsjvCu7CO}8G+E^36H>GD;@xG zXMbqn6&X8H3H^s;g7;mDPjq0m7uS5J@a95oG za?iNuCxGVl2=+cuo)g;vZdbRDws{I3u!Z!<<*6R@@PE`aEH9(*miTKpsPcM)Ho?Ct80i06Tl`5? zb@v6s*emNIB}cMi)nh;Ys!qM^#m;p4g-|Id{b9-pMR^HtOb);HKvj8HM=)IQ7B|PM zHQeYK8D-(BPR&61D+%Dy?%v|(Lt!3a&N+cwGo>wrke4v}#tSuJL{ffY#jh36JQ%Nf zpm{M_Fz0x*fhlp|^C{fuQwIS(ZkHEb_BRFHKS>1L|5qMN>Ry?4zx$AG-M0o<+v*ia z{5cuSf0s%6wUQ37TwWrpRPa7f*QKUbeyt5q1Ps%x53DHuyFn!T`-sls7QH@5PCOhg zUh-JWE{~f_sQe~$fDb0edO9HC#t@j4-VavtxC~q0Dhtqxj`li=A1=zz}~&{=m9fc(N&)}q8mKQf;ChA z25FIh&_%;bfr-l5bft0(&`z2Q=FYbAOKL3n9oei-p5CCgyqdOO^Nt|o#yQQ*ex(AR zPWVeZ?D5}{BQq`p+0JD85h}=I22{!P&OL7#(f{q7kN;?!ZFYz_T6DNav;dnwP zfGD2GM*#V?2AZ$`I-c0xYa!i!F{I0TKc}lV90EOl7K7C2d$dPrW4d^;(O~AQbzsts zGGI=8e=u?Gboizsd34ZP`Ay3`FOjvnb`8+s=3;H#gwm3IEu1AcH-$<1`DbV&_8yf~ zXf{*5tC~l%9TPLO<7UOH-_)(ZUTAm;OR})mI{5WSQA|()^$3a}keudhb@~KH72J7} zr@+nfMi?G+2jC1w_dEiJZ5xR61*J*&7i~do$R4!CMIwp7yJo$N{PajJ{lT^PS>Gqg zAzSy-JJbdyJ?A{hQ(*GV=OxhhNixuFI6U=20?1xXXAK1qJuhJ_fadiG z@;;C*!I1}!B&_s(5_(;C7rNB;9dxs21K>Z$rb1hKKR9&Xj;`n{0g26efW&t7K~nL0 zwD;CwV0zLp`2GM1aRsMG+_FOccLh{<`cT{L;R6ujUq&;3+*9#gKVSBgvqD_sLu<)_ z%QeMIo(8jXMl`{KAuRoY#fOt9f!S& z718j?&3KS>VghYbF%@iSFdR%SwhPagwRC{R@)9mtlZ)2_UTc5Th9+GDturUHE&k`P z9?+&9do(^-lI88Co~hbOs1sBSV*16mzL*$(|qhObw)u}(d>w$x}kQvWY z@L)_K{cC*jEzUT)jSWTb*fA$8{D?;UHO@5ha)u|nkau$V@{2C?JZ3o^Sh6#`rMg3x zeXtzdk5_=nW3R)#-Rsb9+#2TGe!+MitbK)EE|^ZPfe&=>1D9Ya6ge zyyDqz?ZCR5#1+F&OSElPsB3gv#vWXpN{AE`jQLD>aZCfoZZ_oJP(%Y6lW6+ITL-Lw}hZC?#O+~W(9tKNg>!yLe*7vb>ezAVzO(3Tcl)UK1# zfuB4~3(vFx9hF7Yzx1A(bx)eg?sT0a$@=R}=F7^FggQZCKd+WY&buEbh9CKsGU1NX zTZ1hJ)m=X9+V>?#)C11^smr_<3|dpOWi87H~;W3CoKGkM*KBS`C&wpE#?dl zlJib3AC6t<35f$}-<@&r!My44@t>Q)%PN<_-ugi>vmFIb_8kev*Sbi0vkHDKX~Ii% zI|;nPc5B5aJwfMYUYht5nc`Q2-?DpO9g}1_4HmmPwH0r8)0iDLp$8#SP%t*7vk{O> zU~>3@jH+5N3Wtwcor0eJ=fQ{2_0*Hboxyik))IL&+QEY}1?+;Fa9!>WH)Q4+B1C2` z6V4sy%F$uZr&8$J5v zPqeq_IQ;#}c6cfBDRk@D7EIoE2=3hA05jX#f`oBJGjRV(C9ov( zI$V1z5^h@m5hPZ)52rYF1W9qRq+hGJ0T$E&F6uxx74?D%*0!x#5(Eyq#Ck?+)MaPx zV>5R@knHT!U)}o;^6Ca*VP7vCcy^HWCtoz>M?R{U%lc#F8*Xu%yn(w5JW%f_EcW9< z>VD01@MBhWjBdj%Zp9SRzd|=&_qJ;;1Bc$#L@l{K$DFY6BO37+1?C9JNKJjt+pP0W zE}t!Tp+j2Dq&>x5;F@A%;pHt2z{j#In7ZjLoDewz1lb90=r}0Uy@(?|E%sfYc(NGQ0{x)8Ak9ejfwg4@ZIS8 zJ9L^qh`P4h8Kmt_;RH2L!GjShe9Z{U*8|_4HJxau8^z(`S=ZpHdQHKH*1v(NZA-vm z9~;1R;5wMGHw4dv@Zg)8_zqO7)JAK+5lYriKBVqEdY8mG@tjuDzjam$D97&fA+Ip~ z7OZa>JW~aPuc5`|&_{j@5;tSU`Yzh)1Co`&;IZtsA#OpV-QDpqZsl{}TTuTRsWB2` z8<9hP3=$8-Y7!o4TXhL&T48bEau^hzx|71Yr}%9vaa9(@%Yhsbu+*9 zZSQ#juGy3XAIH>$6IN9LDOE>;%+xbbb8H7#ed`2FJF}nkW)<(*lGb<333uw);ZE!_ zn=7D3$KN%Z&-^0UR6`ao>RTEsKdM0DO_GbFze9(@S8B~3TJ_Ww$>0b0HXo`%$H-{VqdjeZya?Hb*S~R_i zF8|F53xYBA%DXzBu$YH0`dJtZRpdnbaGzWSbH)Pjg~vwHcG{kBc&*uRe}&7?A!{*Q z)a((+$k+!ahW`qZyrW2Of`jjm>#tJ(?DW=F5OLq{JRhkJ2mRDPbX(1?Rd$oCRo4w1 z2s#tuz5)1_!^9K-zMMRngf5bK+>9CcCN>xilFO_EHYeW!`;IGfNoURhZsk+pi!3Sn z=wOT+Fq7dX@a5{5gsveu=HZL8TA=E?4`BI(+vbGDJbZCB3qja0E6p8d7fhH1;M;gk zLwKUaUAXkdI5_YA5pXVQ40v&*H*9tFER1-o1k*N?pF7n>)vZc3%D;n|5nFD6>_qem`xqWPRzA>Wn{z z5TXPHTcp@g!J|hBs|xslAGD~NF5jF3R7XSL;_AF$6~XRKKY=6l%A=mas%b^g78hLG z6i7wmvh@PK7YzZYy~!`knz5~D*t*CzXV^}rhHciSiik_hi2!nmO^E)sTK6TpIN=~$?^TwhTh?+yoS-0>H#31iSP>J$4?ljBydV;wNL z?PtcV$nn+mYc-Xc@MIqn7R`<|$$Y}X4_3rqtmtsssi+}W&_3o24`}mFE+1@N=yIAM zxaFl9PMdH9);VVb8Y>Y>@9L$@k;l5aPZ8_*TNid;)xPj8ZNL<7Ujjx51LTVCbWHp!t6C z7HZJdu9u;p;#NKfz9^(7zpiS=wjxQ-lQrhf<$^FCh*=x!qI;-%(?i|&p|G%sx3|Aw zN?6Rp*Ko}iGbC5)2@l8u@GY@07%m7(hx4CRhs^S>VAeqvif)C%>Nbx-;{E*~^@<(o zO>pqtpcn~U-5AZK2H#5-l)bMmd8xMelKow6C2D_G@U!OZvb_R*QFLAfjjV5FJX40{o&$yMKgnVJ%?^`k%WJXpzjCTXt1I?p2Y!YjVi^7IPq z#BH^KZ*i6S&)b6~3mS{FX1d=XLZKQ(SGx}XJES$7yh zCMen{ogkFk!JV4|=~oiBf+}sB>O`Q%CDxP(@PiTY4aKea7hOKZh8j;5p93tS-7A!a z`$}Dcwc2IENfq~ihquFF`)f}@@?o-yXC>%aFm7Ewu?2PT;y+r4w5yUCOGDM2&n(L7 zn%a$>o>G^s*KU-AdjTvKop0fB3G&o6KM0Y@wM5)1D!mbiiv>{oO;Dqk{g6_suQ(+`e;gKidsUc05R;+YJv zYVI27Hy?r>kNyEEJA&{$SP8xsw47Y{?j z$!|IBrVN22w(W$In>m4hZvFy3Bxivg9Z$nX?TJ%lCFohua;jFOQ2(y|RkMHAddWELf^OZTK%#aZNKJ@<5se%{688gSRhQ`{Twp09k-FM4kAP{<{l;x17~6PWVAfEGGghCt$g(C$4VBa{lg;2!>7UfgsBLPSbL<#Rb-~_Ghk@0+luei2!XP@BtoV#*6^IpOok58%&x zA3(3^ws1=GzF^9~6r^0rfHiOHkv^;gJqudSq_6|j%8xc$ds_$Kd-;3y!q%!RhfKhx zHmog~yz`iNdizm?I6*-WDd_Bi$Bz;}HShsH2yE1rbxT;76PF$K!}EN4N%iO3;tEw`U*k;RxCv+k`6dcIfpY%li%EqKN6w@1zRi;c=QOg zHSqyIXi>FXi6Si7Xp5`!f>i{z4{A%@{WBf3UMIAe6Mb(6xwC;EW*+Ag0e&zdzF{{O zCx8eTG2eJVXgpPX5EQ{%RA1Pr-g!9rr@z46@JKNJZFBHjqgAkaeex5{!izaOUAsm7 zC_bfmx$7^<yAt8nzgs5UJ5OT+P ztS9CPFBo0Gx=|jWZEOg&uuU?OMKJM)JmwR(@;R3CKX}RrSx(IcQ82|>P9!YIit-5y zKUlHlL`y3ee7Rg*#4XGj9?<5UTt3*kz{`o<;FR6fVbz&E!ILhFVVSmxAR&1ZIF|7a z>^M0FBp%duZ|irMYoSRV*&X7?~yYc`BM_C+h97l_PPvoT^kP)%KQqhw&?(- z%LampPxY@a6&QR)>Ts7gRN~m}Y@Z!lsh0z7)!Mi#l9Y2Z0uRq8Pj?F|(~A{4761g= z+W3GUw5XcS<|0Rz+yB&*QQE#x5cuOwBzx3yn_SDQ7nLXRnMB7hcLOo;$L z7!ltLA5+L>M=|9*AT*vTJ_{;>r@UUk*_orj;~sZm8!855Mtg&EPwhbFH2sf$|c;-Va!=3hY`7K zB|3*W!vos95ec8gbb))v)P&0D!_Ye=1)L1o0o~M5V0`~UV6l1=n0MhdNLYHE^kyB) zNz_H*pRE;j>_!b&v|!tBxI_IJJX^g~9xIvfk1TNZEI&e&pkQn42_dJIEQaxe7F8>Z z(>nCnHQ>2{73= z4W55fZ&M&YX2cIdOIc1PZ542NepPZ^c@KdQ_!c{X!HBn5Y)pajE4G}}lBds+u!z@s zJM#$(KUfVdCr17YQ3iSSctA zn%6f%HnagIrhxF}#3|203kvaYGrUo#3mE7h4;*_gqXxcON!fPz7JiO{;SJ(e&m1|)=( z2N`7p!8&<6kn9~odJ~xA>!>a@aGa!f#Yj!xr?mKV%3(Gl@D?FPP;f2&&{K zi20$4D%s*h5!#}H>F0%}Bj{SK2I>tYHTxcIQNguLSyIPwT(jA?>Hg~yL%4#O2j9`~ zNJ0n=4HaC^>=Qwbx$q5fx=)l!^fg}QLT9yK~ux5_P*-+;zZN&SywkbgI#ft?^=AdCC z5IUeCqr&ZzY%A{AEWkwQo%HCz0W1OHfgO@(%n38l6F+yRj$ECHB#Aof zl!>;u;M$)8y-!-?;5yPlkJ}R184f!?==Io)p3^}hfb5$o5ik!u^wbj0l@dFLsK=BS z8hSs?dj|)5mIiykE?E8gMv&b164=##J6I6EhV)?_XVHStOR{&PWa;l&|L0Ap6XnjS zC$8-vNmO1`+hv6kq681U#+Z~Z2fg|=k5LCVRzdDGUn}&umr0`^Qq+mS zl->tSiGX?Np)4tep5afUS`K>4PPd1HZ34j-@eWw-wKGWf4hNYrj9FYXAiWx!mCbvUNm3@dFOWCHBZj;DRTU*Ty-5YA?r7 z$A=P^5`hfYG6gW>I1R0Y*y0NL#cABV#GOP##P!VDGDrOUsi}$O2NL3kvHM6UH}sIKozM?t%L=TY@EzR)QxY@`R$5U_+dr!p3QiS*|VYs3h+#Y|)It5?QS! zS-Z#ni|0jH@mcQKL$GOz39&qEDpmZKI^MGmCuF4H&b}7d;O9O~Ossj>Aa@UY+&bzo zWrc)I1`Q!7bqG8;9S3H=Z3rF)So;;p7K4o_z-!<#YQXTeY?U!BsXa|6sgswNm&BhB z${O0ZBVm`ItPY-WJbDBmvIxQtAXKd|%i+bVPNi1;+zH7Uwf+N1bD(295>tTUOK{=Ou>vq5{D4E12;e@!(ev0?j=Ef_zz<3P z7mgmz7n+z@ejp)!d?`n7ZooO%rj8sOn4SU_#V-ZV?~<=`S_w9mvsi(iPxn&Yu9Rgf z5355R$m*v~TK`NEzW!FTAq{R~sS}pNXUbv)Vp8}4R9K7Ux}pfRr3}r9oed@P*1x`# zJI0-VtriQXCXfg$S^CD52=D_9@y#$bQ37!Q7fd;ii)cJme6f`X&OW{s_WWx<`0nPR3-&m&x7bRWyM`X?aQ%V>naEm3zgzy9I zb1jzb;I-7s5v{R^p)Kyr6u|xqi-lt>6BEl1B*c#|6=TiV-xfA-8U+5Z=?W66>;Sj! zS^J%$7PDB6fs4CTqB{4LvBm3*P}erHD+wmV;;^BfU4KX| zYC2ZeWzIj`+1CP_e|l`zI=;POYGTd921QKd_5X-COc{^e$<25^tK$e$`|pJ9i!A_4 z+H$|F75@fY-9|idp*ebvft`F?ON!8xrhAuH;<0T2D^(6AY!bM&p)eu&zy`%doOF<6 zIIhN@2&bMY<{*IrMSaL~NVrx(U}J>`me8{lXkyXolJUfvhYfy)0DGeQ#x2yHO0wytzJ0rV)}w(^39Q}?3xLqA4Zwut0~#V00h;Jiw*g!nrmWR#n z7oSm6O7AiyTwe=p(39x)cLPjKta;d=*e2Jn=VCS%5;l{5sR64y?Et6tHvvm3h{5K^ zJ#t+H57*yEX%iKl49uqMQDE3xbPn*e-*p0k<;MPma8oRD1G3 zZ8!M`EC51>O><0$se|@x1$IyxZuvd7T9o38hVM1sfjfY8|0*Y%FAnVn|c0B z+V~M;VWs0P;J^>J!RkINSS;>=c_ut3?TA-JN)qP8I;YK32h8gyDSL1;Ax7Ytv=*2U z%flw?&~|G2zvxLEQgCNq3v6%#+Qh_~hs~EtKqu_;f`R+if@$OLgY~DkgJawE@4yzE zwV7TeoQfM5s6I3LVAl0j_ay!`R$~DWy0syR8q32bGsTCJ4_r<#!|hI@xdQV{c-H1j@0;S+Z@X$b+_Ggs6Sa8C`JRLr zfm@qan2>y6^BF0D%vu=;GStPtTVd$n&I$q>Yor9YUscmh?Apr*j3?GSY*65ZidXhV zuMi5ENgJg)4z{=G4lA5l23Bo;3l=_WgLx)A*l6F^6#pw}s%d_|mAFj_1zWs3S?&d*nkgWIeeBsj)^ z$)m<#o(ay{h#ht>691hRp=mxjmTh9&M?CSiK)=|XJE4QtZQ9~l$p9>K?8!nppF&`BD$pM2UjI?DySZ zrO7)1+pZ*-bXc&k)BL}!+spsoH2^bydMV*L-!t2!(o_xFOEIM1^UL*#Sa41K|yVXAshKt z`t2dpZanzc{}*sE*xGRkd2MZ>-z5+j-2Wie_SJgz?@_LjGCM0}ov`u4^D8XT&l}En z!i4Y(KdOX+gino2=AN5OHC?UXYLbFGGX;kK=$o0kr zz=rGgAo=`Gkg>Eo=9%!~lIdA1#J@>zYFa)E5m)lu%$A$_mJlOw2a7xymk(@^qUOYf zKnK&$YdajN>1}>Nc%cS}bnyw=$F&Lq8!N;m=m)vc#8SR}mKsm2dDx&I;zk9*M^`Xq zg^WwW_Bw%GetV$v_V2)i4{l&u6nVT{c(CcZrl$DOi*=fMNh#t!B40Kj=`dlFz+uxF z6JmMTq~4525=HF@A-^+-JNrstgT9%GCRP`h7)`8s*nFwDL^fv;NGU!9mYy66c1(-} z2~(w*XTpPx%aP)gzkf;fjrwiWHD`NEifs6c5F>Ed5Z5^$*l_YsERA7^V8+@^V$M@N z?QZInan3)?wSvILD#_#29@=96Y%(>m=3#>(nIfhrE2OoN)@TO(V%vckL#~08XHu~2 z3Hk1d;9wK5e|jVF?;9p-8jO9#x++%(?()>XSX53&eAJ}|`la@F_2N}xe$ucZp zxU+)5#tKfF1|^Tt#BM0M$9Q7R!v-gh33M=Jg@jH2^d4YdnVHaMst1_VrX|QYex9@} zwB&I>LebLVTZezu)JjVe(@7C*tvgQ%F#?ATd7**jVKctl4Fnfz-bvkSG|)cg+SdXb zk&6jf0`hHvjCRK@pe$ioIW6*k#kX{z^OmNnw+1N{c#D|{^(9oro z;)+G@v*k)$C&UOGHY8DFdDx8g%+SH6m;EKQMFn&1D}jwJCBRKARp#+h!^trG3^%;`8lPYQ}_PS+iV$H(_J&#Qu_%~Dtg@jGd z?>9l)%PO#D@dz;9VGKz8g&~qAG$*a;xKhsIV+YS_>Mgj(es7Z@PTk;0h!Hq!$d`Wd zfz4;cHjzVKBY~pUXMVLS{OB=EG_jd;em9<2^RPkr3RDPmh&-rbqnJ2p+=y6tzbZW{ z!Y}-~75>k#6fxr? z5C|miy~-B^48|+`*2;pRiA7m3qlx7Q3F60>%7S%1BnK(nkKDKpKL8Sby9Cn8ZNd{| zCD>TbQe+04m?_TuDMLN$O$kDTpx|O`Df(bC_`&k2mZHyk^4?3__BDOjm8tp4-!L5Vd*at zcihq%Ol^A*I;Y4%qGMAqv*>KhGb_QyVwS=&fX=!?)g87~t<5}`RoS+@_|DqD2{D4Q z7-+`z!i%DdewdJaV8d1W)C^XSa(_$aj6bdGGUp%etRS$l#_Bx!af=79B1}!JdDx(5 zz>wV_1yjajV{$W|&&oK$4ntl-n?aL6y608yi^PLK!s(TyWuaxT0+zIxDL(Y?6-}+F z2gL#Y64q}CdAYr?Lh{_RhhWnm&q_Y9`HZ-vPvx~ps;K_$c3|nkofQN&)`&|`28)|m z^pk+b6KfteID=*BM=0Cdi@?mx7-;v?uOQjKIY{&yfO#f7Ytv-hJgT~VMKv2xIcxO= zd-3yj`d{i0xV0g(k`HV+!T(fi)7qu4o>=p+`BGW1 zR(Jja6Sg0Q4({$?LY*0`KoQSXtyP zmIbp9teNywJpaQS^|GtW2@!&FtPJN0iF`07!*U`|jr`YGa7@_)D~P=AOTb3cWnSbh z`z7EOANzyf4y1vd0aY-Yt)%5GCi0GfqVg_c&F!O_GELhC?yU7r{b2n!6H}07etzLc z@w-mnb6XNu;K?-e{gS48(-8+d6Abw!y$kK`5 z<}0LN{PiZ#u3-z1WDh}ND0#)Fm0)AB#IJq8I~XBOUOiL2Z6AqTh3y=LVEN?-%cnX= zjh{ZzAtN7fSi@3q>0IOK;s*huv!Hn>Lxaoj90e`z3SL}m0%i<|!fdvZ$Xm=(*f(2z z=X>$47Q@wv)5+3+utlD{ywZXq|Lp2lBJ#R;z-YS6i#$%j5=&O7cz|x>2c~T330~j4 z_;rf>7(6V87+JKOgt$%-A!s>;TrCqi|K%5SdCYRfQ^) z`UYGZ*YR)Ms8Pd~wd>>Cii9W>syJ!5QV}hc4V5cp5&GM0$z3in@zK)ID49w{24hahRVs2UB*R$)LdbYEDJAs*mkPRp?=lXoZ6Shuy-+6y;o6RP{VQyo48(m598Xgg%P^jYS zs0K&LWy+Ye6p!AmgPKRlR4OK%kt@^Yq}51F8l z)!=sBB9w|@(pW_l6FP!B^w150iHVRZWpVPf)?oJnR2HVV43#TG6e=bT-6hfunHwBe zKTau^OQYp+k%}IX=o%96($`fXCflwxO1M_LyC%%NmX(3SniCA zDn3Ldi;j(wa|d3!J=8a8&O1~Qm3>_rG6MC)-R5I>E=oRB9>tx^bz_Vo4BhCWlt)Nq zQL&L|Jg(>6qR2C)Y$X4k`=NckY$)&1Lg%VX3MKjzC#6+aE9``zDs(fPN zqouJjr7S8+9wp5gjk{776Q+pf2txd7Uo*uiMmk6lg1B5PS`nj&Q^d%np^6w)oKnWb z#G&(k+*YoXaWkjjSd{CM7ZV+;P{zq(Lgmu95wU0>PGRz)OeoqbI#d?LgmK;I2Ep-2 z&dEWilp^vNSqMi=D0d}7p=3D9(x#+%gv(`d@yhIG&}}}5`P@CyIE9oOQ=|gTi2h`x zQm%?rM4>tKjE;|rV>pVC2t=Uyc9U~mqgmlv=MH^2L5&*0-Jpn#)rpECJ|+zH;llMD z6(S2Aj1F8m;*c22RnoNfb}7!%ASCCU(4@Fynb+|;MT9Tph7a3+Eri*zz9SCSJR5%L&0H}q`2k<&Sx z?S|X24~>n-`^Cd$Occi_DHEgPx-=p-Udn`#du{9CJ9FmS#1&CY9LH6esYopbOGixl+m?Awn`l>gxw(N>uc>)gObq@s3c+!Wel>oHPf~ggHeb zLb8#~VJXUpae0AY&^B(`1)&8sp&}RU>({GCy2c&oaADMed#q9sDp#p=7$DU}Ve)Vp zCz`kiL)AlWa*)NxDexVl_*jG&CAsRS({@ARWifFK2boAtS4PVQ%ayo-TTTV3N6ES4$pu?7hTP_i++b?B*d~&j>>D&{jC;tdC&_Jgjs5f9g;gxM%a+_^ zWCZnh5RIa@-1>`TBnF0Df0b0tV|DfS5T1A<%dCoxV&bGx3Y8w6`Ux^=xJXNO)oWma z-IeliWH*p!5l(KM*5fd7zRKzeN4PZsMQY_m{b081XsV6CNX2=*E8#ijyurZI~;Zts7 z$o)lgETc5qgjP53e@K6>jqCWME?d?noGGHS6w+`d=k##yq*4~4AD+R?Bkiv<|D-Lw ze7T{EIsOT`Y}b(Rz*T2DWXcG+p_a=_g()Uu*Oattq>f96F)E$Alw(Y!2CfaMqaUh~ zFGTJrirhLyd|YgNoX!{{C5dH0@0XL?joouXizwTvQkjyZ+-60@x%LRJq^iCfB73=O z7mLT!%;XOi>eUDjW-4{msI0CMMQ4}xl*an`!OCl1RI_9 zA`}~4a782=o%7byY;=Y5BM8w(m%IqoMi+`@U%+G=UG-Gu&~0?yFAw2HH@FYug55ad zN%zP;Ydq^-*{2B;2oqhiFXWnc@9cAWCXl;*vac90fn3VzfiQtwD2D5poO{olzKkc| zFZ+BRCSarzOrTiMBqkigqZBf|mi+%ip>P9FF@FT+s58<;i{g06lBRiNbY{9R zU72o7ccur^lj+6uX8JIFnSM-vW&ksgkut%Ij0s^vnJ~IT~H>cvaoH(Lr98%v9#qkm`=FeyBli(~G*NIf z?#M~wnvumspaB|iwz+@nzv!(y>dw+-w@mLlEP)JE*k$J32%DT~HC)7jhqPVsA1%eSv3?`$1Q*DX|0TO{XSM(l?r5 zM;E?u4lb8~)u-t6Awz!AjV>{UqB^~AXxj|;=Z)lA7fwrua=Cp2mk;^iWC&*9oAG`) zm@hlI$G(&^*j)Qh_FacLlKxSDYnYIWRFMX!1AlCF1b6l=z}FTN^X1{|b!HX4=;y7p ztl=J5qQcTV63U&xgApiv%?Zob1K$Y0?m@*~uBTVhorB!Q+=PB9tH9B~OZ5B=vxA)M zpMg!3C!Pma!NGT4;2SVwz+vs(Oa`p+xy0US(@4^#yGYaVP6p+sQ3j@U{-)q-hiB@G z!#C*R;*#{#fX%eOZyV^E_3O6?Uo*n;^}ttl^+k}!!(H_1JHvw<8h3-Gdv*o~H(sDK zrcI;YkKPY9et=jgga%(X@$|ZV!Q_!;wPzCC!Lr)pwaFC%C9Ups*926VK{;Ju#kCyD zvEJ(5?8ieS@g*!P>}G%)JWa@rCXOdRkWtlKZZvo7V#@bK=WyG+?zCIbfwh!H66q{b(@=?Sn&fM4baft#`D6R&Rg&RdE9PP7Z-dhpX2Qo)K~dB$SLJ1 zz50YF{bryPmc3jQ?5jMV&RBbneiZ8iPTjr(e&o(^AKaLARDfN5vc6GR58OjMa#91J zT(wPmChQ5A=R8ZB(6g)h+{96uy3@2^$%#7ZnZ3MNTi?p;lfCUIJD<)HhjBv)wfa!l zFhC-H<<}KZ=Z@1b4M-ahobYP{RGC*BaM#?-!G%{Vcm%DUdxcK&nF-Fl4~7H#drS7a z?!f{|wGPI+xwd%ZrnG~{IBpyUcbnA?+w^}UyqM`$A{rqpRcwuFFn&cFHZ{}EV)Oj#S+W^4ZfMxXc4t+s# zP$S%^we(0s++kt$h?ls<%mZNdmTB4r&nke0Ef1(OnpXoAy4Tfay?iKMJ9Dz+{mYw@ z%{GSv>dU+W&p+LxJ+w83?dtl6rs^&aJP2X=7G8Q;(CuL=kVbLD2mBfZRnMhS5QMmM zD1B#An{eknFwAiCyqd);Xhx(nEPlQ--LLJFm;fFPgxWSN_;x-WySW|NK8Zd8LGxhlS&(-_)4QDUAUz5BX;wHY9p(50NBR-82CWv1H zVVg)63OLas5e~O+QVZc`c{Ptm&@k^G=Q*RXaNEjtZDGEZi_j~qftK9r=iqp`tRdg>68tJ z=!+Q*Af5CcEV|v39)B&7K2dosI6Qm-m~ngqcrNZh@4xB?lKYe(Bd|(#)2_b7d>RjN ziJEV~ltY`elc&A}v;KEf-7NZ*B&Dsr_9uCgIHBS=$%^J+2Wu9| z<-t%J)%X&LJPV^ou*ZTt4p%%(Gad_qP#%vZhumD56z)Fa!QghydMX~sQvsE>+i3^> z7^L%5as*J*?R_;v0n~XYMgnMFk01{Pxo1d!80rn=u^5(udM2N$NBmcI_mB#hQJ(5TD%Vu5u?ML;TkHPHzGrtik1*JbsCDu?J zRx*Mod|j@8x7Wv+5#4=+6NYj7mpUETnu-WRJw)B6F} z53{ukYTlIGc^E97aiFebaY%IL!tLwCO%F}cu8`MeYbDpwIM3fls1}q?;0sofUU0!A z@#_Ut{Y&OYzU!I{Zr)0uZ62KfUcDvTe*sx6(-1?5LfiK@=~FcMXMJ!707anIo@P9$L_8*U5gh46_SpYHJr zs<7_{{o`+8^onf*=(#gDg5=VXutJC(J!5qlI@M1CCe8N+nh%*E@nAcUSmQW7DJ=mU zA3uhU-?bS_fwlY*3+6{$+ogjRT~=z--_8M}M~~Kuf0?ab=(t~FJE}XF>T*~;XUuA~ zY+zM3GqAVh=9!V=C+Wl)5>}4Zv|EZa5b^B!H4v(tO9OGoB%a~+Ni2iC6ST!r!Rw)T z1o^CdK)WAV1$wRuhL!x&sqyD8VqvF>G{L*MwzySO8s}Tdk6;1Z*J_420p!;#Xuizl zM+|c%gu9rtJf4hc0pimq9zlMSp3qraHqqhgzVxbwC*b#gl>l=Gm!Z3RzN8nf|-zh}P2rGp@_E^BxvwE7d*6zDpQJs1=kiV_FQ59&y72eS#jr`hpWeqEFB{ z?wIHl+^iWrV%xSA&^-GUUKqL$6zR7Vi!?Z6>y7u(mg;bY9O70Bqes57{0Q|=kIg7Q zqVrU81Q6v%3NG{Ai9C-R!RY@JkM(lb>l8VBLMfh|8dHltC zI^cw&7Mxdi9<1Yh8H}G*0{n4EY>0cqeJoJ|)d7;blPaR;QQ3z}XRvLit=Br8&&W@# z%g6A@={OzjOPjP5E8Rx+V>0@2}BEPlECX_~QxkWgvcSfTfPCm{=yBpnqxl z0?Z!i4OgvO1#I4J#)1q|j~>DMXe-{jh#X=WH>Dj4nwR*>;t5bXXaR~RsKI^4VgU?X z(Ccz80kr>^Xe@yIS_93O;d-k(VPD>O0zI~`E10u9+QYN}@#zl_`oWfcbpPm0^zaww z==Puqn4;Pb{yF!O?%S_89T3?AOx)=Oc31nCZr<1rOsjvCu7CO}8G+E^36H>GD;@xG zXMbqn6&X8H3H^s;g7;mDPjq0m7uS5J@a95oG za?iNuCxGVl2=+cuo)g;vZdbRDws{I3u!Z!<<*6R@@PE`aEH9(*miTKpsPcM)Ho?Ct80i06Tl`5? zb@v6s*emNIB}cMi)nh;Ys!qM^#m;p4g-|Id{b9-pMR^HtOb);HKvj8HM=)IQ7B|PM zHQeYK8D-(BPR&61D+%Dy?%v|(Lt!3a&N+cwGo>wrke4v}#tSuJL{ffY#jh36JQ%Nf zpm{M_Fz0x*fhlp|^C{fuQwIS(ZkHEb_BRFHKS>1L|5qMN>Ry?4zx$AG-M0o<+v*ia z{5cuSf0s%6wUQ37TwWrpRPa7f*QKUbeyt5q1Ps%x53DHuyFn!T`-sls7QH@5PCOhg zUh-JWE{~f_sQe~$fDb0edO9HC#t@j4-VavtxC~q0Dhtqxj`li=A1=zz}~&{=m9fc(N&)}q8mKQf;ChA z25FIh&_%;bfr-l5bft0(&`z2Q=FYbAOKL3n9oei-p5CCgyqdOO^Nt|o#yQQ*ex(AR zPWVeZ?D5}{BQq`p+0JD85h}=I22{!P&OL7#(f{q7kN;?!ZFYz_T6DNav;dnwP zfGD2GM*#V?2AZ$`I-c0xYa!i!F{I0TKc}lV90EOl7K7C2d$dPrW4d^;(O~AQbzsts zGGI=8e=u?Gboizsd34ZP`Ay3`FOjvnb`8+s=3;H#gwm3IEu1AcH-$<1`DbV&_8yf~ zXf{*5tC~l%9TPLO<7UOH-_)(ZUTAm;OR})mI{5WSQA|()^$3a}keudhb@~KH72J7} zr@+nfMi?G+2jC1w_dEiJZ5xR61*J*&7i~do$R4!CMIwp7yJo$N{PajJ{lT^PS>Gqg zAzSy-JJbdyJ?A{hQ(*GV=OxhhNixuFI6U=20?1xXXAK1qJuhJ_fadiG z@;;C*!I1}!B&_s(5_(;C7rNB;9dxs21K>Z$rb1hKKR9&Xj;`n{0g26efW&t7K~nL0 zwD;CwV0zLp`2GM1aRsMG+_FOccLh{<`cT{L;R6ujUq&;3+*9#gKVSBgvqD_sLu<)_ z%QeMIo(8jXMl`{KAuRoY#fOt9f!S& z718j?&3KS>VghYbF%@iSFdR%SwhPagwRC{R@)9mtlZ)2_UTc5Th9+GDturUHE&k`P z9?+&9do(^-lI88Co~hbOs1sBSV*16mzL*$(|qhObw)u}(d>w$x}kQvWY z@L)_K{cC*jEzUT)jSWTb*fA$8{D?;UHO@5ha)u|nkau$V@{2C?JZ3o^Sh6#`rMg3x zeXtzdk5_=nW3R)#-Rsb9+#2TGe!+MitbK)EE|^ZPfe&=>1D9Ya6ge zyyDqz?ZCR5#1+F&OSElPsB3gv#vWXpN{AE`jQLD>aZCfoZZ_oJP(%Y6lW6+ITL-Lw}hZC?#O+~W(9tKNg>!yLe*7vb>ezAVzO(3Tcl)UK1# zfuB4~3(vFx9hF7Yzx1A(bx)eg?sT0a$@=R}=F7^FggQZCKd+WY&buEbh9CKsGU1NX zTZ1hJ)m=X9+V>?#)C11^smr_<3|dpOWi87H~;W3CoKGkM*KBS`C&wpE#?dl zlJib3AC6t<35f$}-<@&r!My44@t>Q)%PN<_-ugi>vmFIb_8kev*Sbi0vkHDKX~Ii% zI|;nPc5B5aJwfMYUYht5nc`Q2-?DpO9g}1_4HmmPwH0r8)0iDLp$8#SP%t*7vk{O> zU~>3@jH+5N3Wtwcor0eJ=fQ{2_0*Hboxyik))IL&+QEY}1?+;Fa9!>WH)Q4+B1C2` z6V4sy%F$uZr&8$J5v zPqeq_IQ;#}c6cfBDRk@D7EIoE2=3hA05jX#f`oBJGjRV(C9ov( zI$V1z5^h@m5hPZ)52rYF1W9qRq+hGJ0T$E&F6uxx74?D%*0!x#5(Eyq#Ck?+)MaPx zV>5R@knHT!U)}o;^6Ca*VP7vCcy^HWCtoz>M?R{U%lc#F8*Xu%yn(w5JW%f_EcW9< z>VD01@MBhWjBdj%Zp9SRzd|=&_qJ;;1Bc$#L@l{K$DFY6BO37+1?C9JNKJjt+pP0W zE}t!Tp+j2Dq&>x5;F@A%;pHt2z{j#In7ZjLoDewz1lb90=r}0Uy@(?|E%sfYc(NGQ0{x)8Ak9ejfwg4@ZIS8 zJ9L^qh`P4h8Kmt_;RH2L!GjShe9Z{U*8|_4HJxau8^z(`S=ZpHdQHKH*1v(NZA-vm z9~;1R;5wMGHw4dv@Zg)8_zqO7)JAK+5lYriKBVqEdY8mG@tjuDzjam$D97&fA+Ip~ z7OZa>JW~aPuc5`|&_{j@5;tSU`Yzh)1Co`&;IZtsA#OpV-QDpqZsl{}TTuTRsWB2` z8<9hP3=$8-Y7!o4TXhL&T48bEau^hzx|71Yr}%9vaa9(@%Yhsbu+*9 zZSQ#juGy3XAIH>$6IN9LDOE>;%+xbbb8H7#ed`2FJF}nkW)<(*lGb<333uw);ZE!_ zn=7D3$KN%Z&-^0UR6`ao>RTEsKdM0DO_GbFze9(@S8B~3TJ_Ww$>0b0HXo`%$H-{VqdjeZya?Hb*S~R_i zF8|F53xYBA%DXzBu$YH0`dJtZRpdnbaGzWSbH)Pjg~vwHcG{kBc&*uRe}&7?A!{*Q z)a((+$k+!ahW`qZyrW2Of`jjm>#tJ(?DW=F5OLq{JRhkJ2mRDPbX(1?Rd$oCRo4w1 z2s#tuz5)1_!^9K-zMMRngf5bK+>9CcCN>xilFO_EHYeW!`;IGfNoURhZsk+pi!3Sn z=wOT+Fq7dX@a5{5gsveu=HZL8TA=E?4`BI(+vbGDJbZCB3qja0E6p8d7fhH1;M;gk zLwKUaUAXkdI5_YA5pXVQ40v&*H*9tFER1-o1k*N?pF7n>)vZc3%D;n|5nFD6>_qem`xqWPRzA>Wn{z z5TXPHTcp@g!J|hBs|xslAGD~NF5jF3R7XSL;_AF$6~XRKKY=6l%A=mas%b^g78hLG z6i7wmvh@PK7YzZYy~!`knz5~D*t*CzXV^}rhHciSiik_hi2!nmO^E)sTK6TpIN=~$?^TwhTh?+yoS-0>H#31iSP>J$4?ljBydV;wNL z?PtcV$nn+mYc-Xc@MIqn7R`<|$$Y}X4_3rqtmtsssi+}W&_3o24`}mFE+1@N=yIAM zxaFl9PMdH9);VVb8Y>Y>@9L$@k;l5aPZ8_*TNid;)xPj8ZNL<7Ujjx51LTVCbWHp!t6C z7HZJdu9u;p;#NKfz9^(7zpiS=wjxQ-lQrhf<$^FCh*=x!qI;-%(?i|&p|G%sx3|Aw zN?6Rp*Ko}iGbC5)2@l8u@GY@07%m7(hx4CRhs^S>VAeqvif)C%>Nbx-;{E*~^@<(o zO>pqtpcn~U-5AZK2H#5-l)bMmd8xMelKow6C2D_G@U!OZvb_R*QFLAfjjV5FJX40{o&$yMKgnVJ%?^`k%WJXpzjCTXt1I?p2Y!YjVi^7IPq z#BH^KZ*i6S&)b6~3mS{FX1d=XLZKQ(SGx}XJES$7yh zCMen{ogkFk!JV4|=~oiBf+}sB>O`Q%CDxP(@PiTY4aKea7hOKZh8j;5p93tS-7A!a z`$}Dcwc2IENfq~ihquFF`)f}@@?o-yXC>%aFm7Ewu?2PT;y+r4w5yUCOGDM2&n(L7 zn%a$>o>G^s*KU-AdjTvKop0fB3G&o6KM0Y@wM5)1D!mbiiv>{oO;Dqk{g6_suQ(+`e;gKidsUc05R;+YJv zYVI27Hy?r>kNyEEJA&{$SP8xsw47Y{?j z$!|IBrVN22w(W$In>m4hZvFy3Bxivg9Z$nX?TJ%lCFohua;jFOQ2(y|RkMHAddWELf^OZTK%#aZNKJ@<5se%{688gSRhQ`{Twp09k-FM4kAP{<{l;x17~6PWVAfEGGghCt$g(C$4VBa{lg;2!>7UfgsBLPSbL<#Rb-~_Ghk@0+luei2!XP@BtoV#*6^IpOok58%&x zA3(3^ws1=GzF^9~6r^0rfHiOHkv^;gJqudSq_6|j%8xc$ds_$Kd-;3y!q%!RhfKhx zHmog~yz`iNdizm?I6*-WDd_Bi$Bz;}HShsH2yE1rbxT;76PF$K!}EN4N%iO3;tEw`U*k;RxCv+k`6dcIfpY%li%EqKN6w@1zRi;c=QOg zHSqyIXi>FXi6Si7Xp5`!f>i{z4{A%@{WBf3UMIAe6Mb(6xwC;EW*+Ag0e&zdzF{{O zCx8eTG2eJVXgpPX5EQ{%RA1Pr-g!9rr@z46@JKNJZFBHjqgAkaeex5{!izaOUAsm7 zC_bfmx$7^<yAt8nzgs5UJ5OT+P ztS9CPFBo0Gx=|jWZEOg&uuU?OMKJM)JmwR(@;R3CKX}RrSx(IcQ82|>P9!YIit-5y zKUlHlL`y3ee7Rg*#4XGj9?<5UTt3*kz{`o<;FR6fVbz&E!ILhFVVSmxAR&1ZIF|7a z>^M0FBp%duZ|irMYoSRV*&X7?~yYc`BM_C+h97l_PPvoT^kP)%KQqhw&?(- z%LampPxY@a6&QR)>Ts7gRN~m}Y@Z!lsh0z7)!Mi#l9Y2Z0uRq8Pj?F|(~A{4761g= z+W3GUw5XcS<|0Rz+yB&*QQE#x5cuOwBzx3yn_SDQ7nLXRnMB7hcLOo;$L z7!ltLA5+L>M=|9*AT*vTJ_{;>r@UUk*_orj;~sZm8!855Mtg&EPwhbFH2sf$|c;-Va!=3hY`7K zB|3*W!vos95ec8gbb))v)P&0D!_Ye=1)L1o0o~M5V0`~UV6l1=n0MhdNLYHE^kyB) zNz_H*pRE;j>_!b&v|!tBxI_IJJX^g~9xIvfk1TNZEI&e&pkQn42_dJIEQaxe7F8>Z z(>nCnHQ>2{73= z4W55fZ&M&YX2cIdOIc1PZ542NepPZ^c@KdQ_!c{X!HBn5Y)pajE4G}}lBds+u!z@s zJM#$(KUfVdCr17YQ3iSSctA zn%6f%HnagIrhxF}#3|203kvaYGrUo#3mE7h4;*_gqXxcON!fPz7JiO{;SJ(e&m1|)=( z2N`7p!8&<6kn9~odJ~xA>!>a@aGa!f#Yj!xr?mKV%3(Gl@D?FPP;f2&&{K zi20$4D%s*h5!#}H>F0%}Bj{SK2I>tYHTxcIQNguLSyIPwT(jA?>Hg~yL%4#O2j9`~ zNJ0n=4HaC^>=Qwbx$q5fx=)l!^fg}QLT9yK~ux5_P*-+;zZN&SywkbgI#ft?^=AdCC z5IUeCqr&ZzY%A{AEWkwQo%HCz0W1OHfgO@(%n38l6F+yRj$ECHB#Aof zl!>;u;M$)8y-!-?;5yPlkJ}R184f!?==Io)p3^}hfb5$o5ik!u^wbj0l@dFLsK=BS z8hSs?dj|)5mIiykE?E8gMv&b164=##J6I6EhV)?_XVHStOR{&PWa;l&|L0Ap6XnjS zC$8-vNmO1`+hv6kq681U#+Z~Z2fg|=k5LCVRzdDGUn}&umr0`^Qq+mS zl->tSiGX?Np)4tep5afUS`K>4PPd1HZ34j-@eWw-wKGWf4hNYrj9FYXAiWx!mCbvUNm3@dFOWCHBZj;DRTU*Ty-5YA?r7 z$A=P^5`hfYG6gW>I1R0Y*y0NL#cABV#GOP##P!VDGDrOUsi}$O2NL3kvHM6UH}sIKozM?t%L=TY@EzR)QxY@`R$5U_+dr!p3QiS*|VYs3h+#Y|)It5?QS! zS-Z#ni|0jH@mcQKL$GOz39&qEDpmZKI^MGmCuF4H&b}7d;O9O~Ossj>Aa@UY+&bzo zWrc)I1`Q!7bqG8;9S3H=Z3rF)So;;p7K4o_z-!<#YQXTeY?U!BsXa|6sgswNm&BhB z${O0ZBVm`ItPY-WJbDBmvIxQtAXKd|%i+bVPNi1;+zH7Uwf+N1bD(295>tTUOK{=Ou>vq5{D4E12;e@!(ev0?j=Ef_zz<3P z7mgmz7n+z@ejp)!d?`n7ZooO%rj8sOn4SU_#V-ZV?~<=`S_w9mvsi(iPxn&Yu9Rgf z5355R$m*v~TK`NEzW!FTAq{R~sS}pNXUbv)Vp8}4R9K7Ux}pfRr3}r9oed@P*1x`# zJI0-VtriQXCXfg$S^CD52=D_9@y#$bQ37!Q7fd;ii)cJme6f`X&OW{s_WWx<`0nPR3-&m&x7bRWyM`X?aQ%V>naEm3zgzy9I zb1jzb;I-7s5v{R^p)Kyr6u|xqi-lt>6BEl1B*c#|6=TiV-xfA-8U+5Z=?W66>;Sj! zS^J%$7PDB6fs4CTqB{4LvBm3*P}erHD+wmV;;^BfU4KX| zYC2ZeWzIj`+1CP_e|l`zI=;POYGTd921QKd_5X-COc{^e$<25^tK$e$`|pJ9i!A_4 z+H$|F75@fY-9|idp*ebvft`F?ON!8xrhAuH;<0T2D^(6AY!bM&p)eu&zy`%doOF<6 zIIhN@2&bMY<{*IrMSaL~NVrx(U}J>`me8{lXkyXolJUfvhYfy)0DGeQ#x2yHO0wytzJ0rV)}w(^39Q}?3xLqA4Zwut0~#V00h;Jiw*g!nrmWR#n z7oSm6O7AiyTwe=p(39x)cLPjKta;d=*e2Jn=VCS%5;l{5sR64y?Et6tHvvm3h{5K^ zJ#t+H57*yEX%iKl49uqMQDE3xbPn*e-*p0k<;MPma8oRD1G3 zZ8!M`EC51>O><0$se|@x1$IyxZuvd7T9o38hVM1sfjfY8|0*Y%FAnVn|c0B z+V~M;VWs0P;J^>J!RkINSS;>=c_ut3?TA-JN)qP8I;YK32h8gyDSL1;Ax7Ytv=*2U z%flw?&~|G2zvxLEQgCNq3v6%#+Qh_~hs~EtKqu_;f`R+if@$OLgY~DkgJawE@4yzE zwV7TeoQfM5s6I3LVAl0j_ay!`R$~DWy0syR8q32bGsTCJ4_r<#!|hI@xdQV{c-H1j@0;S+Z@X$b+_Ggs6Sa8C`JRLr zfm@qan2>y6^BF0D%vu=;GStPtTVd$n&I$q>Yor9YUscmh?Apr*j3?GSY*65ZidXhV zuMi5ENgJg)4z{=G4lA5l23Bo;3l=_WgLx)A*l6F^6#pw}s%d_|mAFj_1zWs3S?&d*nkgWIeeBsj)^ z$)m<#o(ay{h#ht>691hRp=mxjmTh9&M?CSiK)=|XJE4QtZQ9~l$p9>K?8!nppF&`BD$pM2UjI?DySZ zrO7)1+pZ*-bXc&k)BL}!+spsoH2^bydMV*L-!t2!(o_xFOEIM1^UL*#Sa41K|yVXAshKt z`t2dpZanzc{}*sE*xGRkd2MZ>-z5+j-2Wie_SJgz?@_LjGCM0}ov`u4^D8XT&l}En z!i4Y(KdOX+gino2=AN5OHC?UXYLbFGGX;kK=$o0kr zz=rGgAo=`Gkg>Eo=9%!~lIdA1#J@>zYFa)E5m)lu%$A$_mJlOw2a7xymk(@^qUOYf zKnK&$YdajN>1}>Nc%cS}bnyw=$F&Lq8!N;m=m)vc#8SR}mKsm2dDx&I;zk9*M^`Xq zg^WwW_Bw%GetV$v_V2)i4{l&u6nVT{c(CcZrl$DOi*=fMNh#t!B40Kj=`dlFz+uxF z6JmMTq~4525=HF@A-^+-JNrstgT9%GCRP`h7)`8s*nFwDL^fv;NGU!9mYy66c1(-} z2~(w*XTpPx%aP)gzkf;fjrwiWHD`NEifs6c5F>Ed5Z5^$*l_YsERA7^V8+@^V$M@N z?QZInan3)?wSvILD#_#29@=96Y%(>m=3#>(nIfhrE2OoN)@TO(V%vckL#~08XHu~2 z3Hk1d;9wK5e|jVF?;9p-8jO9#x++%(?()>XSX53&eAJ}|`la@F_2N}xe$ucZp zxU+)5#tKfF1|^Tt#BM0M$9Q7R!v-gh33M=Jg@jH2^d4YdnVHaMst1_VrX|QYex9@} zwB&I>LebLVTZezu)JjVe(@7C*tvgQ%F#?ATd7**jVKctl4Fnfz-bvkSG|)cg+SdXb zk&6jf0`hHvjCRK@pe$ioIW6*k#kX{z^OmNnw+1N{c#D|{^(9oro z;)+G@v*k)$C&UOGHY8DFdDx8g%+SH6m;EKQMFn&1D}jwJCBRKARp#+h!^trG3^%;`8lPYQ}_PS+iV$H(_J&#Qu_%~Dtg@jGd z?>9l)%PO#D@dz;9VGKz8g&~qAG$*a;xKhsIV+YS_>Mgj(es7Z@PTk;0h!Hq!$d`Wd zfz4;cHjzVKBY~pUXMVLS{OB=EG_jd;em9<2^RPkr3RDPmh&-rbqnJ2p+=y6tzbZW{ z!Y}-~75>k#6fxr? z5C|miy~-B^48|+`*2;pRiA7m3qlx7Q3F60>%7S%1BnK(nkKDKpKL8Sby9Cn8ZNd{| zCD>TbQe+04m?_TuDMLN$O$kDTpx|O`Df(bC_`&k2mZHyk^4?3__BDOjm8tp4-!L5Vd*at zcihq%Ol^A*I;Y4%qGMAqv*>KhGb_QyVwS=&fX=!?)g87~t<5}`RoS+@_|DqD2{D4Q z7-+`z!i%DdewdJaV8d1W)C^XSa(_$aj6bdGGUp%etRS$l#_Bx!af=79B1}!JdDx(5 zz>wV_1yjajV{$W|&&oK$4ntl-n?aL6y608yi^PLK!s(TyWuaxT0+zIxDL(Y?6-}+F z2gL#Y64q}CdAYr?Lh{_RhhWnm&q_Y9`HZ-vPvx~ps;K_$c3|nkofQN&)`&|`28)|m z^pk+b6KfteID=*BM=0Cdi@?mx7-;v?uOQjKIY{&yfO#f7Ytv-hJgT~VMKv2xIcxO= zd-3yj`d{i0xV0g(k`HV+!T(fi)7qu4o>=p+`BGW1 zR(Jja6Sg0Q4({$?LY*0`KoQSXtyP zmIbp9teNywJpaQS^|GtW2@!&FtPJN0iF`07!*U`|jr`YGa7@_)D~P=AOTb3cWnSbh z`z7EOANzyf4y1vd0aY-Yt)%5GCi0GfqVg_c&F!O_GELhC?yU7r{b2n!6H}07etzLc z@w-mnb6XNu;K?-e{gS48(-8+d6Abw!y$kK`5 z<}0LN{PiZ#u3-z1WDh}ND0#)Fm0)AB#IJq8I~XBOUOiL2Z6AqTh3y=LVEN?-%cnX= zjh{ZzAtN7fSi@3q>0IOK;s*huv!Hn>Lxaoj90e`z3SL}m0%i<|!fdvZ$Xm=(*f(2z z=X>$47Q@wv)5+3+utlD{ywZXq|Lp2lBJ#R;z-YS6i#$%j5=&O7cz|x>2c~T330~j4 z_;rf>7(6V87+JKOgt$%-A!s>;TrCqi|K%5SdCYRfQ^) z`UYGZ*YR)Ms8Pd~wd>>Cii9W>syJ!5QV}hc4V5cp5&GM0$z3in@zK)ID49w{24hahRVs2UB*R$)LdbYEDJAs*mkPRp?=lXoZ6Shuy-+6y;o6RP{VQyo48(m598Xgg%P^jYS zs0K&LWy+Ye6p!AmgPKRlR4OK%kt@^Yq}51F8l z)!=sBB9w|@(pW_l6FP!B^w150iHVRZWpVPf)?oJnR2HVV43#TG6e=bT-6hfunHwBe zKTau^OQYp+k%}IX=o%96($`fXCflwxO1M_LyC%%NmX(3SniCA zDn3Ldi;j(wa|d3!J=8a8&O1~Qm3>_rG6MC)-R5I>E=oRB9>tx^bz_Vo4BhCWlt)Nq zQL&L|Jg(>6qR2C)Y$X4k`=NckY$)&1Lg%VX3MKjzC#6+aE9``zDs(fPN zqouJjr7S8+9wp5gjk{776Q+pf2txd7Uo*uiMmk6lg1B5PS`nj&Q^d%np^6w)oKnWb z#G&(k+*YoXaWkjjSd{CM7ZV+;P{zq(Lgmu95wU0>PGRz)OeoqbI#d?LgmK;I2Ep-2 z&dEWilp^vNSqMi=D0d}7p=3D9(x#+%gv(`d@yhIG&}}}5`P@CyIE9oOQ=|gTi2h`x zQm%?rM4>tKjE;|rV>pVC2t=Uyc9U~mqgmlv=MH^2L5&*0-Jpn#)rpECJ|+zH;llMD z6(S2Aj1F8m;*c22RnoNfb}7!%ASCCU(4@Fynb+|;MT9Tph7a3+Eri*zz9SCSJR5%L&0H}q`2k<&Sx z?S|X24~>n-`^Cd$Occi_DHEgPx-=p-Udn`#du{9CJ9FmS#1&CY9LH6esYopbOGixl+m?Awn`l>gxw(N>uc>)gObq@s3c+!Wel>oHPf~ggHeb zLb8#~VJXUpae0AY&^B(`1)&8sp&}RU>({GCy2c&oaADMed#q9sDp#p=7$DU}Ve)Vp zCz`kiL)AlWa*)NxDexVl_*jG&CAsRS({@ARWifFK2boAtS4PVQ%ayo-TTTV3N6ES4$pu?7hTP_i++b?B*d~&j>>D&{jC;tdC&_Jgjs5f9g;gxM%a+_^ zWCZnh5RIa@-1>`TBnF0Df0b0tV|DfS5T1A<%dCoxV&bGx3Y8w6`Ux^=xJXNO)oWma z-IeliWH*p!5l(KM*5fd7zRKzeN4PZsMQY_m{b081XsV6CNX2=*E8#ijyurZI~;Zts7 z$o)lgETc5qgjP53e@K6>jqCWME?d?noGGHS6w+`d=k##yq*4~4AD+R?Bkiv<|D-Lw ze7T{EIsOT`Y}b(Rz*T2DWXcG+p_a=_g()Uu*Oattq>f96F)E$Alw(Y!2CfaMqaUh~ zFGTJrirhLyd|YgNoX!{{C5dH0@0XL?joouXizwTvQkjyZ+-60@x%LRJq^iCfB73=O z7mLT!%;XOi>eUDjW-4{msI0CMMQ4}xl*an`!OCl1RI_9 zA`}~4a782=o%7byY;=Y5BM8w(m%IqoMi+`@U%+G=UG-Gu&~0?yFAw2HH@FYug55ad zN%zP;Ydq^-*{2B;2oqhiFXWnc@9cAWCXl;*vac90fn3VzfiQtwD2D5poO{olzKkc| zFZ+BRCSarzOrTiMBqkigqZBf|mi+%ip>P9FF@FT+s58<;i{g06lBRiNbY{9R zU72o7ccur^lj+6uX8JIFnSM-vW&ksgkut%Ij0s^vnJ~IT~H>cvaoH(Lr98%v9#qkm`=FeyBli(~G*NIf z?#M~wnvumspaB|iwz+@nzv!(y>dw+-w@mLlEP)JE*k$J32%DT~HC)7jhqPVsA1%eSv3?`$1Q*DX|0TO{XSM(l?r5 zM;E?u4lb8~)u-t6Awz!AjV>{UqB^~AXxj|;=Z)lA7fwrua=Cp2mk;^iWC&*9oAG`) zm@hlI$G(&^*j)Qh_FacLlKxSDYnYIWRFMX!1AlCF1b6l=z}FTN^X1{|b!HX4=;y7p ztl=J5qQcTV63U&xgApiv%?Zob1K$Y0?m@*~uBTVhorB!Q+=PB9tH9B~OZ5B=vxA)M zpMg!3C!Pma!NGT4;2SVwz+vs(Oa`p+xy0US(@4^#yGYaVP6p+sQ3j@U{-)q-hiB@G z!#C*R;*#{#fX%eOZyV^E_3O6?Uo*n;^}ttl^+k}!!(H_1JHvw<8h3-Gdv*o~H(sDK zrcI;YkKPY9et=jgga%(X@$|ZV!Q_!;wPzCC!Lr)pwaFC%C9Ups*926VK{;Ju#kCyD zvEJ(5?8ieS@g*!P>}G%)JWa@rCXOdRkWtlKZZvo7V#@bK=WyG+?zCIbfwh!H66q{b(@=?Sn&fM4baft#`D6R&Rg&RdE9PP7Z-dhpX2Qo)K~dB$SLJ1 zz50YF{bryPmc3jQ?5jMV&RBbneiZ8iPTjr(e&o(^AKaLARDfN5vc6GR58OjMa#91J zT(wPmChQ5A=R8ZB(6g)h+{96uy3@2^$%#7ZnZ3MNTi?p;lfCUIJD<)HhjBv)wfa!l zFhC-H<<}KZ=Z@1b4M-ahobYP{RGC*BaM#?-!G%{Vcm%DUdxcK&nF-Fl4~7H#drS7a z?!f{|wGPI+xwd%ZrnG~{IBpyUcbnA?+w^}UyqM`$A{rqpRcwuFFn&cFHZ{}EV)Oj#S+W^4ZfMxXc4t+s# zP$S%^we(0s++kt$h?ls<%mZNdmTB4r&nke0Ef1(OnpXoAy4Tfay?iKMJ9Dz+{mYw@ z%{GSv>dU+W&p+LxJ+w83?dtl6rs^&aJP2X=7G8Q;(CuL=kVbLD2mBfZRnMhS5QMmM zD1B#An{eknFwAiCyqd);Xhx(nEPlQ--LLJFm;fFPgxWSN_;x-WySW|NK8Zd8LGxhlS&(-_)4QDUAUz5BX;wHY9p(50NBR-82CWv1H zVVg)63OLas5e~O+QVZc`c{Ptm&@k^G=Q*RXaNEjtZDGEZi_j~qftK9r=iqp`tRdg>68tJ z=!+Q*Af5CcEV|v39)B&7K2dosI6Qm-m~ngqcrNZh@4xB?lKYe(Bd|(#)2_b7d>RjN ziJEV~ltY`elc&A}v;KEf-7NZ*B&Dsr_9uCgIHBS=$%^J+2Wu9| z<-t%J)%X&LJPV^ou*ZTt4p%%(Gad_qP#%vZhumD56z)Fa!QghydMX~sQvsE>+i3^> z7^L%5as*J*?R_;v0n~XYMgnMFk01{Pxo1d!80rn=u^5(udM2N$NBmcI_mB#hQJ(5TD%Vu5u?ML;TkHPHzGrtik1*JbsCDu?J zRx*Mod|j@8x7Wv+5#4=+6NYj7mpUETnu-WRJw)B6F} z53{ukYTlIGc^E97aiFebaY%IL!tLwCO%F}cu8`MeYbDpwIM3fls1}q?;0sofUU0!A z@#_Ut{Y&OYzU!I{Zr)0uZ62KfUcDvTe*sx6(-1?5LfiK@=~FcMXMJ!707anIo@P9$L_8*U5gh46_SpYHJr zs<7_{{o`+8^onf*=(#gDg5=VXutJC(J!5qlI@M1CCe8N+nh%*E@nAcUSmQW7DJ=mU zA3uhU-?bS_fwlY*3+6{$+ogjRT~=z--_8M}M~~Kuf0?ab=(t~FJE}XF>T*~;XUuA~ zY+zM3GqAVh=9!V=C+Wl)5>}4Zv|EZa5b^B!H4v(tO9OGoB%a~+Ni2iC6ST!r!Rw)T z1o^CdK)WAV1$wRuhL!x&sqyD8VqvF>G{L*MwzySO8s}Tdk6;1Z*J_420p!;#Xuizl zM+|c%gu9rtJf4hc0pimq9zlMSp3qraHqqhgzVxbwC*b#gl>l=Gm!Z3RzN8nf|-zh}P2rGp@_E^BxvwE7d*6zDpQJs1=kiV_FQ59&y72eS#jr`hpWeqEFB{ z?wIHl+^iWrV%xSA&^-GUUKqL$6zR7Vi!?Z6>y7u(mg;bY9O70Bqes57{0Q|=kIg7Q zqVrU81Q6v%3NG{Ai9C-R!RY@JkM(lb>l8VBLMfh|8dHltC zI^cw&7Mxdi9<1Yh8H}G*0{n4EY>0cqeJoJ|)d7;blPaR;QQ3z}XRvLit=Br8&&W@# z%g6A@={OzjOPjP5E8Rx+V>0@2}BEPlECX_~QxkWgvcSfTfPCm{=yBpnqxl z0?Z!i4OgvO1#I4J#)1q|j~>DMXe-{jh#X=WH>Dj4nwR*>;t5bXXaR~RsKI^4VgU?X z(Ccz80kr>^Xe@yIS_93O;d-k(VPD>O0zI~`E10u9+QYN}@#zl_`oWfcbpPm0^zaww z==Puqn4;Pb{yF!O?%S_89T3?AOx)=Oc31nCZr<1rOsjvCu7CO}8G+E^36H>GD;@xG zXMbqn6&X8H3H^s;g7;mDPjq0m7uS5J@a95oG za?iNuCxGVl2=+cuo)g;vZdbRDws{I3u!Z!<<*6R@@PE`aEH9(*miTKpsPcM)Ho?Ct80i06Tl`5? zb@v6s*emNIB}cMi)nh;Ys!qM^#m;p4g-|Id{b9-pMR^HtOb);HKvj8HM=)IQ7B|PM zHQeYK8D-(BPR&61D+%Dy?%v|(Lt!3a&N+cwGo>wrke4v}#tSuJL{ffY#jh36JQ%Nf zpm{M_Fz0x*fhlp|^C{fuQwIS(ZkHEb_BRFHKS>1L|5qMN>Ry?4zx$AG-M0o<+v*ia z{5cuSf0s%6wUQ37TwWrpRPa7f*QKUbeyt5q1Ps%x53DHuyFn!T`-sls7QH@5PCOhg zUh-JWE{~f_sQe~$fDb0edO9HC#t@j4-VavtxC~q0Dhtqxj`li=A1=zz}~&{=m9fc(N&)}q8mKQf;ChA z25FIh&_%;bfr-l5bft0(&`z2Q=FYbAOKL3n9oei-p5CCgyqdOO^Nt|o#yQQ*ex(AR zPWVeZ?D5}{BQq`p+0JD85h}=I22{!P&OL7#(f{q7kN;?!ZFYz_T6DNav;dnwP zfGD2GM*#V?2AZ$`I-c0xYa!i!F{I0TKc}lV90EOl7K7C2d$dPrW4d^;(O~AQbzsts zGGI=8e=u?Gboizsd34ZP`Ay3`FOjvnb`8+s=3;H#gwm3IEu1AcH-$<1`DbV&_8yf~ zXf{*5tC~l%9TPLO<7UOH-_)(ZUTAm;OR})mI{5WSQA|()^$3a}keudhb@~KH72J7} zr@+nfMi?G+2jC1w_dEiJZ5xR61*J*&7i~do$R4!CMIwp7yJo$N{PajJ{lT^PS>Gqg zAzSy-JJbdyJ?A{hQ(*GV=OxhhNixuFI6U=20?1xXXAK1qJuhJ_fadiG z@;;C*!I1}!B&_s(5_(;C7rNB;9dxs21K>Z$rb1hKKR9&Xj;`n{0g26efW&t7K~nL0 zwD;CwV0zLp`2GM1aRsMG+_FOccLh{<`cT{L;R6ujUq&;3+*9#gKVSBgvqD_sLu<)_ z%QeMIo(8jXMl`{KAuRoY#fOt9f!S& z718j?&3KS>VghYbF%@iSFdR%SwhPagwRC{R@)9mtlZ)2_UTc5Th9+GDturUHE&k`P z9?+&9do(^-lI88Co~hbOs1sBSV*16mzL*$(|qhObw)u}(d>w$x}kQvWY z@L)_K{cC*jEzUT)jSWTb*fA$8{D?;UHO@5ha)u|nkau$V@{2C?JZ3o^Sh6#`rMg3x zeXtzdk5_=nW3R)#-Rsb9+#2TGe!+MitbK)EE|^ZPfe&=>1D9Ya6ge zyyDqz?ZCR5#1+F&OSElPsB3gv#vWXpN{AE`jQLD>aZCfoZZ_oJP(%Y6lW6+ITL-Lw}hZC?#O+~W(9tKNg>!yLe*7vb>ezAVzO(3Tcl)UK1# zfuB4~3(vFx9hF7Yzx1A(bx)eg?sT0a$@=R}=F7^FggQZCKd+WY&buEbh9CKsGU1NX zTZ1hJ)m=X9+V>?#)C11^smr_<3|dpOWi87H~;W3CoKGkM*KBS`C&wpE#?dl zlJib3AC6t<35f$}-<@&r!My44@t>Q)%PN<_-ugi>vmFIb_8kev*Sbi0vkHDKX~Ii% zI|;nPc5B5aJwfMYUYht5nc`Q2-?DpO9g}1_4HmmPwH0r8)0iDLp$8#SP%t*7vk{O> zU~>3@jH+5N3Wtwcor0eJ=fQ{2_0*Hboxyik))IL&+QEY}1?+;Fa9!>WH)Q4+B1C2` z6V4sy%F$uZr&8$J5v zPqeq_IQ;#}c6cfBDRk@D7EIoE2=3hA05jX#f`oBJGjRV(C9ov( zI$V1z5^h@m5hPZ)52rYF1W9qRq+hGJ0T$E&F6uxx74?D%*0!x#5(Eyq#Ck?+)MaPx zV>5R@knHT!U)}o;^6Ca*VP7vCcy^HWCtoz>M?R{U%lc#F8*Xu%yn(w5JW%f_EcW9< z>VD01@MBhWjBdj%Zp9SRzd|=&_qJ;;1Bc$#L@l{K$DFY6BO37+1?C9JNKJjt+pP0W zE}t!Tp+j2Dq&>x5;F@A%;pHt2z{j#In7ZjLoDewz1lb90=r}0Uy@(?|E%sfYc(NGQ0{x)8Ak9ejfwg4@ZIS8 zJ9L^qh`P4h8Kmt_;RH2L!GjShe9Z{U*8|_4HJxau8^z(`S=ZpHdQHKH*1v(NZA-vm z9~;1R;5wMGHw4dv@Zg)8_zqO7)JAK+5lYriKBVqEdY8mG@tjuDzjam$D97&fA+Ip~ z7OZa>JW~aPuc5`|&_{j@5;tSU`Yzh)1Co`&;IZtsA#OpV-QDpqZsl{}TTuTRsWB2` z8<9hP3=$8-Y7!o4TXhL&T48bEau^hzx|71Yr}%9vaa9(@%Yhsbu+*9 zZSQ#juGy3XAIH>$6IN9LDOE>;%+xbbb8H7#ed`2FJF}nkW)<(*lGb<333uw);ZE!_ zn=7D3$KN%Z&-^0UR6`ao>RTEsKdM0DO_GbFze9(@S8B~3TJ_Ww$>0b0HXo`%$H-{VqdjeZya?Hb*S~R_i zF8|F53xYBA%DXzBu$YH0`dJtZRpdnbaGzWSbH)Pjg~vwHcG{kBc&*uRe}&7?A!{*Q z)a((+$k+!ahW`qZyrW2Of`jjm>#tJ(?DW=F5OLq{JRhkJ2mRDPbX(1?Rd$oCRo4w1 z2s#tuz5)1_!^9K-zMMRngf5bK+>9CcCN>xilFO_EHYeW!`;IGfNoURhZsk+pi!3Sn z=wOT+Fq7dX@a5{5gsveu=HZL8TA=E?4`BI(+vbGDJbZCB3qja0E6p8d7fhH1;M;gk zLwKUaUAXkdI5_YA5pXVQ40v&*H*9tFER1-o1k*N?pF7n>)vZc3%D;n|5nFD6>_qem`xqWPRzA>Wn{z z5TXPHTcp@g!J|hBs|xslAGD~NF5jF3R7XSL;_AF$6~XRKKY=6l%A=mas%b^g78hLG z6i7wmvh@PK7YzZYy~!`knz5~D*t*CzXV^}rhHciSiik_hi2!nmO^E)sTK6TpIN=~$?^TwhTh?+yoS-0>H#31iSP>J$4?ljBydV;wNL z?PtcV$nn+mYc-Xc@MIqn7R`<|$$Y}X4_3rqtmtsssi+}W&_3o24`}mFE+1@N=yIAM zxaFl9PMdH9);VVb8Y>Y>@9L$@k;l5aPZ8_*TNid;)xPj8ZNL<7Ujjx51LTVCbWHp!t6C z7HZJdu9u;p;#NKfz9^(7zpiS=wjxQ-lQrhf<$^FCh*=x!qI;-%(?i|&p|G%sx3|Aw zN?6Rp*Ko}iGbC5)2@l8u@GY@07%m7(hx4CRhs^S>VAeqvif)C%>Nbx-;{E*~^@<(o zO>pqtpcn~U-5AZK2H#5-l)bMmd8xMelKow6C2D_G@U!OZvb_R*QFLAfjjV5FJX40{o&$yMKgnVJ%?^`k%WJXpzjCTXt1I?p2Y!YjVi^7IPq z#BH^KZ*i6S&)b6~3mS{FX1d=XLZKQ(SGx}XJES$7yh zCMen{ogkFk!JV4|=~oiBf+}sB>O`Q%CDxP(@PiTY4aKea7hOKZh8j;5p93tS-7A!a z`$}Dcwc2IENfq~ihquFF`)f}@@?o-yXC>%aFm7Ewu?2PT;y+r4w5yUCOGDM2&n(L7 zn%a$>o>G^s*KU-AdjTvKop0fB3G&o6KM0Y@wM5)1D!mbiiv>{oO;Dqk{g6_suQ(+`e;gKidsUc05R;+YJv zYVI27Hy?r>kNyEEJA&{$SP8xsw47Y{?j z$!|IBrVN22w(W$In>m4hZvFy3Bxivg9Z$nX?TJ%lCFohua;jFOQ2(y|RkMHAddWELf^OZTK%#aZNKJ@<5se%{688gSRhQ`{Twp09k-FM4kAP{<{l;x17~6PWVAfEGGghCt$g(C$4VBa{lg;2!>7UfgsBLPSbL<#Rb-~_Ghk@0+luei2!XP@BtoV#*6^IpOok58%&x zA3(3^ws1=GzF^9~6r^0rfHiOHkv^;gJqudSq_6|j%8xc$ds_$Kd-;3y!q%!RhfKhx zHmog~yz`iNdizm?I6*-WDd_Bi$Bz;}HShsH2yE1rbxT;76PF$K!}EN4N%iO3;tEw`U*k;RxCv+k`6dcIfpY%li%EqKN6w@1zRi;c=QOg zHSqyIXi>FXi6Si7Xp5`!f>i{z4{A%@{WBf3UMIAe6Mb(6xwC;EW*+Ag0e&zdzF{{O zCx8eTG2eJVXgpPX5EQ{%RA1Pr-g!9rr@z46@JKNJZFBHjqgAkaeex5{!izaOUAsm7 zC_bfmx$7^<yAt8nzgs5UJ5OT+P ztS9CPFBo0Gx=|jWZEOg&uuU?OMKJM)JmwR(@;R3CKX}RrSx(IcQ82|>P9!YIit-5y zKUlHlL`y3ee7Rg*#4XGj9?<5UTt3*kz{`o<;FR6fVbz&E!ILhFVVSmxAR&1ZIF|7a z>^M0FBp%duZ|irMYoSRV*&X7?~yYc`BM_C+h97l_PPvoT^kP)%KQqhw&?(- z%LampPxY@a6&QR)>Ts7gRN~m}Y@Z!lsh0z7)!Mi#l9Y2Z0uRq8Pj?F|(~A{4761g= z+W3GUw5XcS<|0Rz+yB&*QQE#x5cuOwBzx3yn_SDQ7nLXRnMB7hcLOo;$L z7!ltLA5+L>M=|9*AT*vTJ_{;>r@UUk*_orj;~sZm8!855Mtg&EPwhbFH2sf$|c;-Va!=3hY`7K zB|3*W!vos95ec8gbb))v)P&0D!_Ye=1)L1o0o~M5V0`~UV6l1=n0MhdNLYHE^kyB) zNz_H*pRE;j>_!b&v|!tBxI_IJJX^g~9xIvfk1TNZEI&e&pkQn42_dJIEQaxe7F8>Z z(>nCnHQ>2{73= z4W55fZ&M&YX2cIdOIc1PZ542NepPZ^c@KdQ_!c{X!HBn5Y)pajE4G}}lBds+u!z@s zJM#$(KUfVdCr17YQ3iSSctA zn%6f%HnagIrhxF}#3|203kvaYGrUo#3mE7h4;*_gqXxcON!fPz7JiO{;SJ(e&m1|)=( z2N`7p!8&<6kn9~odJ~xA>!>a@aGa!f#Yj!xr?mKV%3(Gl@D?FPP;f2&&{K zi20$4D%s*h5!#}H>F0%}Bj{SK2I>tYHTxcIQNguLSyIPwT(jA?>Hg~yL%4#O2j9`~ zNJ0n=4HaC^>=Qwbx$q5fx=)l!^fg}QLT9yK~ux5_P*-+;zZN&SywkbgI#ft?^=AdCC z5IUeCqr&ZzY%A{AEWkwQo%HCz0W1OHfgO@(%n38l6F+yRj$ECHB#Aof zl!>;u;M$)8y-!-?;5yPlkJ}R184f!?==Io)p3^}hfb5$o5ik!u^wbj0l@dFLsK=BS z8hSs?dj|)5mIiykE?E8gMv&b164=##J6I6EhV)?_XVHStOR{&PWa;l&|L0Ap6XnjS zC$8-vNmO1`+hv6kq681U#+Z~Z2fg|=k5LCVRzdDGUn}&umr0`^Qq+mS zl->tSiGX?Np)4tep5afUS`K>4PPd1HZ34j-@eWw-wKGWf4hNYrj9FYXAiWx!mCbvUNm3@dFOWCHBZj;DRTU*Ty-5YA?r7 z$A=P^5`hfYG6gW>I1R0Y*y0NL#cABV#GOP##P!VDGDrOUsi}$O2NL3kvHM6UH}sIKozM?t%L=TY@EzR)QxY@`R$5U_+dr!p3QiS*|VYs3h+#Y|)It5?QS! zS-Z#ni|0jH@mcQKL$GOz39&qEDpmZKI^MGmCuF4H&b}7d;O9O~Ossj>Aa@UY+&bzo zWrc)I1`Q!7bqG8;9S3H=Z3rF)So;;p7K4o_z-!<#YQXTeY?U!BsXa|6sgswNm&BhB z${O0ZBVm`ItPY-WJbDBmvIxQtAXKd|%i+bVPNi1;+zH7Uwf+N1bD(295>tTUOK{=Ou>vq5{D4E12;e@!(ev0?j=Ef_zz<3P z7mgmz7n+z@ejp)!d?`n7ZooO%rj8sOn4SU_#V-ZV?~<=`S_w9mvsi(iPxn&Yu9Rgf z5355R$m*v~TK`NEzW!FTAq{R~sS}pNXUbv)Vp8}4R9K7Ux}pfRr3}r9oed@P*1x`# zJI0-VtriQXCXfg$S^CD52=D_9@y#$bQ37!Q7fd;ii)cJme6f`X&OW{s_WWx<`0nPR3-&m&x7bRWyM`X?aQ%V>naEm3zgzy9I zb1jzb;I-7s5v{R^p)Kyr6u|xqi-lt>6BEl1B*c#|6=TiV-xfA-8U+5Z=?W66>;Sj! zS^J%$7PDB6fs4CTqB{4LvBm3*P}erHD+wmV;;^BfU4KX| zYC2ZeWzIj`+1CP_e|l`zI=;POYGTd921QKd_5X-COc{^e$<25^tK$e$`|pJ9i!A_4 z+H$|F75@fY-9|idp*ebvft`F?ON!8xrhAuH;<0T2D^(6AY!bM&p)eu&zy`%doOF<6 zIIhN@2&bMY<{*IrMSaL~NVrx(U}J>`me8{lXkyXolJUfvhYfy)0DGeQ#x2yHO0wytzJ0rV)}w(^39Q}?3xLqA4Zwut0~#V00h;Jiw*g!nrmWR#n z7oSm6O7AiyTwe=p(39x)cLPjKta;d=*e2Jn=VCS%5;l{5sR64y?Et6tHvvm3h{5K^ zJ#t+H57*yEX%iKl49uqMQDE3xbPn*e-*p0k<;MPma8oRD1G3 zZ8!M`EC51>O><0$se|@x1$IyxZuvd7T9o38hVM1sfjfY8|0*Y%FAnVn|c0B z+V~M;VWs0P;J^>J!RkINSS;>=c_ut3?TA-JN)qP8I;YK32h8gyDSL1;Ax7Ytv=*2U z%flw?&~|G2zvxLEQgCNq3v6%#+Qh_~hs~EtKqu_;f`R+if@$OLgY~DkgJawE@4yzE zwV7TeoQfM5s6I3LVAl0j_ay!`R$~DWy0syR8q32bGsTCJ4_r<#!|hI@xdQV{c-H1j@0;S+Z@X$b+_Ggs6Sa8C`JRLr zfm@qan2>y6^BF0D%vu=;GStPtTVd$n&I$q>Yor9YUscmh?Apr*j3?GSY*65ZidXhV zuMi5ENgJg)4z{=G4lA5l23Bo;3l=_WgLx)A*l6F^6#pw}s%d_|mAFj_1zWs3S?&d*nkgWIeeBsj)^ z$)m<#o(ay{h#ht>691hRp=mxjmTh9&M?CSiK)=|XJE4QtZQ9~l$p9>K?8!nppF&`BD$pM2UjI?DySZ zrO7)1+pZ*-bXc&k)BL}!+spsoH2^bydMV*L-!t2!(o_xFOEIM1^UL*#Sa41K|yVXAshKt z`t2dpZanzc{}*sE*xGRkd2MZ>-z5+j-2Wie_SJgz?@_LjGCM0}ov`u4^D8XT&l}En z!i4Y(KdOX+gino2=AN5OHC?UXYLbFGGX;kK=$o0kr zz=rGgAo=`Gkg>Eo=9%!~lIdA1#J@>zYFa)E5m)lu%$A$_mJlOw2a7xymk(@^qUOYf zKnK&$YdajN>1}>Nc%cS}bnyw=$F&Lq8!N;m=m)vc#8SR}mKsm2dDx&I;zk9*M^`Xq zg^WwW_Bw%GetV$v_V2)i4{l&u6nVT{c(CcZrl$DOi*=fMNh#t!B40Kj=`dlFz+uxF z6JmMTq~4525=HF@A-^+-JNrstgT9%GCRP`h7)`8s*nFwDL^fv;NGU!9mYy66c1(-} z2~(w*XTpPx%aP)gzkf;fjrwiWHD`NEifs6c5F>Ed5Z5^$*l_YsERA7^V8+@^V$M@N z?QZInan3)?wSvILD#_#29@=96Y%(>m=3#>(nIfhrE2OoN)@TO(V%vckL#~08XHu~2 z3Hk1d;9wK5e|jVF?;9p-8jO9#x++%(?()>XSX53&eAJ}|`la@F_2N}xe$ucZp zxU+)5#tKfF1|^Tt#BM0M$9Q7R!v-gh33M=Jg@jH2^d4YdnVHaMst1_VrX|QYex9@} zwB&I>LebLVTZezu)JjVe(@7C*tvgQ%F#?ATd7**jVKctl4Fnfz-bvkSG|)cg+SdXb zk&6jf0`hHvjCRK@pe$ioIW6*k#kX{z^OmNnw+1N{c#D|{^(9oro z;)+G@v*k)$C&UOGHY8DFdDx8g%+SH6m;EKQMFn&1D}jwJCBRKARp#+h!^trG3^%;`8lPYQ}_PS+iV$H(_J&#Qu_%~Dtg@jGd z?>9l)%PO#D@dz;9VGKz8g&~qAG$*a;xKhsIV+YS_>Mgj(es7Z@PTk;0h!Hq!$d`Wd zfz4;cHjzVKBY~pUXMVLS{OB=EG_jd;em9<2^RPkr3RDPmh&-rbqnJ2p+=y6tzbZW{ z!Y}-~75>k#6fxr? z5C|miy~-B^48|+`*2;pRiA7m3qlx7Q3F60>%7S%1BnK(nkKDKpKL8Sby9Cn8ZNd{| zCD>TbQe+04m?_TuDMLN$O$kDTpx|O`Df(bC_`&k2mZHyk^4?3__BDOjm8tp4-!L5Vd*at zcihq%Ol^A*I;Y4%qGMAqv*>KhGb_QyVwS=&fX=!?)g87~t<5}`RoS+@_|DqD2{D4Q z7-+`z!i%DdewdJaV8d1W)C^XSa(_$aj6bdGGUp%etRS$l#_Bx!af=79B1}!JdDx(5 zz>wV_1yjajV{$W|&&oK$4ntl-n?aL6y608yi^PLK!s(TyWuaxT0+zIxDL(Y?6-}+F z2gL#Y64q}CdAYr?Lh{_RhhWnm&q_Y9`HZ-vPvx~ps;K_$c3|nkofQN&)`&|`28)|m z^pk+b6KfteID=*BM=0Cdi@?mx7-;v?uOQjKIY{&yfO#f7Ytv-hJgT~VMKv2xIcxO= zd-3yj`d{i0xV0g(k`HV+!T(fi)7qu4o>=p+`BGW1 zR(Jja6Sg0Q4({$?LY*0`KoQSXtyP zmIbp9teNywJpaQS^|GtW2@!&FtPJN0iF`07!*U`|jr`YGa7@_)D~P=AOTb3cWnSbh z`z7EOANzyf4y1vd0aY-Yt)%5GCi0GfqVg_c&F!O_GELhC?yU7r{b2n!6H}07etzLc z@w-mnb6XNu;K?-e{gS48(-8+d6Abw!y$kK`5 z<}0LN{PiZ#u3-z1WDh}ND0#)Fm0)AB#IJq8I~XBOUOiL2Z6AqTh3y=LVEN?-%cnX= zjh{ZzAtN7fSi@3q>0IOK;s*huv!Hn>Lxaoj90e`z3SL}m0%i<|!fdvZ$Xm=(*f(2z z=X>$47Q@wv)5+3+utlD{ywZXq|Lp2lBJ#R;z-YS6i#$%j5=&O7cz|x>2c~T330~j4 z_;rf>7(6V87+JKOgt$%-A!s>;TrCqi|K%5SdCYRfQ^) z`UYGZ*YR)Ms8Pd~wd>>Cii9W>syJ!5QV}hc4V5cp5&GM0$z3in@zK)ID49w{24hahRVs 0).astype(int) + + # 保存测试数据 + data_dir = Path("dataset/dataset_processed/test_data") + data_dir.mkdir(parents=True, exist_ok=True) + + df = pd.DataFrame(X, columns=[f'feature_{i}' for i in range(4)]) + df['label'] = y + + data_path = data_dir / "test_data.csv" + df.to_csv(data_path, index=False) + + return str(data_path) + + @pytest.fixture + def trained_model(self, sample_data): + # 训练一个简单的模型用于测试 + from sklearn.ensemble import RandomForestClassifier + + # 加载数据 + data = pd.read_csv(sample_data) + X = data.drop('label', axis=1).values + y = data['label'].values + + # 训练模型 + model = RandomForestClassifier(n_estimators=10, random_state=42) + model.fit(X, y) + + # 使用MLflow记录模型 + with mlflow.start_run() as run: + mlflow.sklearn.log_model(model, "model") + mlflow.log_param("algorithm", "RandomForestClassifier") + + return run.info.run_id + + def test_predict(self, model_manager, sample_data, trained_model): + # 设置输出路径 + output_dir = Path("predictions/test") + output_dir.mkdir(parents=True, exist_ok=True) + output_path = str(output_dir / "test_predictions.csv") + + # 执行预测 + result = model_manager.predict( + run_id=trained_model, + data_path=sample_data, + output_path=output_path, + metrics=['accuracy', 'f1'] + ) + + # 验证结果 + assert result['status'] == 'success' + assert 'prediction' in result + assert Path(result['prediction']['output_file']).exists() + assert result['prediction']['samples_count'] == 100 + assert 'accuracy' in result['prediction']['metrics'] + assert 'f1' in result['prediction']['metrics'] + + # 验证预测结果格式 + predictions = pd.read_csv(output_path) + assert 'prediction' in predictions.columns + assert len(predictions) == 100 + + def test_predict_invalid_run_id(self, model_manager, sample_data): + result = model_manager.predict( + run_id="invalid_run_id", + data_path=sample_data, + output_path="predictions/test/invalid.csv" + ) + + assert result['status'] == 'error' + assert '未找到运行ID' in result['message'] + + def test_predict_invalid_data_path(self, model_manager, trained_model): + result = model_manager.predict( + run_id=trained_model, + data_path="invalid/path/data.csv", + output_path="predictions/test/invalid.csv" + ) + + assert result['status'] == 'error' + assert '数据加载失败' in result['message'] \ No newline at end of file